mimas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/Gemfile +12 -0
  4. data/Gemfile.lock +106 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +346 -0
  7. data/Rakefile +4 -0
  8. data/bin/mimas +5 -0
  9. data/lib/mimas/archiver.rb +68 -0
  10. data/lib/mimas/base_command.rb +22 -0
  11. data/lib/mimas/cli.rb +22 -0
  12. data/lib/mimas/commands/console.rb +21 -0
  13. data/lib/mimas/commands/init.rb +63 -0
  14. data/lib/mimas/commands/install.rb +21 -0
  15. data/lib/mimas/commands/provision.rb +50 -0
  16. data/lib/mimas/commands/push.rb +28 -0
  17. data/lib/mimas/commands/setup/caddy.rb +18 -0
  18. data/lib/mimas/commands/setup/deploy_user.rb +16 -0
  19. data/lib/mimas/commands/setup/ruby.rb +17 -0
  20. data/lib/mimas/commands/setup.rb +10 -0
  21. data/lib/mimas/config.rb +124 -0
  22. data/lib/mimas/deployment/caddy.rb +39 -0
  23. data/lib/mimas/deployment/files.rb +51 -0
  24. data/lib/mimas/deployment/paths.rb +54 -0
  25. data/lib/mimas/deployment/ruby_app.rb +126 -0
  26. data/lib/mimas/deployment/static_site.rb +28 -0
  27. data/lib/mimas/deployment.rb +27 -0
  28. data/lib/mimas/hooks.rb +25 -0
  29. data/lib/mimas/plugins.rb +19 -0
  30. data/lib/mimas/scripts/configure_ruby.sh +19 -0
  31. data/lib/mimas/scripts/create_deploy_user.sh +30 -0
  32. data/lib/mimas/scripts/harden.sh +125 -0
  33. data/lib/mimas/scripts/install_caddy.sh +51 -0
  34. data/lib/mimas/scripts/install_ruby_prerequisites.sh +26 -0
  35. data/lib/mimas/server/caddy.rb +17 -0
  36. data/lib/mimas/server/console.rb +34 -0
  37. data/lib/mimas/server/deploy_user.rb +11 -0
  38. data/lib/mimas/server/harden.rb +20 -0
  39. data/lib/mimas/server/prerequisites.rb +25 -0
  40. data/lib/mimas/server/ruby.rb +37 -0
  41. data/lib/mimas/server.rb +37 -0
  42. data/lib/mimas/site.rb +24 -0
  43. data/lib/mimas/ssh.rb +78 -0
  44. data/lib/mimas/template.rb +28 -0
  45. data/lib/mimas/templates/Caddyfile.ruby.erb +12 -0
  46. data/lib/mimas/templates/Caddyfile.static.erb +15 -0
  47. data/lib/mimas/templates/caddy.service.erb +19 -0
  48. data/lib/mimas/templates/config.rb.erb +51 -0
  49. data/lib/mimas/templates/falcon.rb.erb +12 -0
  50. data/lib/mimas/templates/mimas.env.erb +2 -0
  51. data/lib/mimas/templates/puma.rb +11 -0
  52. data/lib/mimas/templates/systemd.service.erb +20 -0
  53. data/lib/mimas/terminal/ansi.rb +71 -0
  54. data/lib/mimas/terminal/printer.rb +14 -0
  55. data/lib/mimas/version.rb +3 -0
  56. data/lib/mimas.rb +48 -0
  57. data/mimas.gemspec +29 -0
  58. data/scripts/release +8 -0
  59. data/scripts/test +7 -0
  60. data/tmp/.keep +0 -0
  61. metadata +181 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6cf8ecafcf4c63ec2a4f1325f280b0cc58e8f131846d3e7292ec350bbc191f57
4
+ data.tar.gz: cd14353d7c99bf62e9d18c450b91c360cfa662b908da8ed68cc0924fe2d86fd9
5
+ SHA512:
6
+ metadata.gz: 7d62835764c60b57e811fdcecfc61c9df9e08c35f123d8ad871b6e2b2299432c18eb215e223a48e3be6bb8839c79d26f87e220caee5b83edb90b3f43166fb884
7
+ data.tar.gz: 1887653d2942eb2f734e60cf3be27aa1857ec0ef4615556fdbe2a88108627be5502ee757fa1816cef5c303bada25b3f542fbc351af58b22ef91898a62e2cb42c
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ .bundle/
2
+ .ruby-gemset
3
+ *.gem
4
+
5
+ .DS_Store
6
+
7
+ /vendor
8
+ */tmp/*
9
+ .vscode
10
+ .yardoc
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "rake"
6
+
7
+ group :test do
8
+ gem "debug"
9
+ gem "minitest"
10
+ gem "minitest-profile"
11
+ gem "minitest-reporters"
12
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,106 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ mimas (0.1.0)
5
+ bcrypt_pbkdf (~> 1.0)
6
+ dry-cli (~> 1.4)
7
+ ed25519 (~> 1.2)
8
+ logger (~> 1.7)
9
+ net-scp (~> 4.1)
10
+ net-ssh (~> 7.3)
11
+
12
+ GEM
13
+ remote: https://rubygems.org/
14
+ specs:
15
+ ansi (1.6.0)
16
+ bcrypt_pbkdf (1.1.2)
17
+ builder (3.3.0)
18
+ date (3.5.1)
19
+ debug (1.11.1)
20
+ irb (~> 1.10)
21
+ reline (>= 0.3.8)
22
+ drb (2.2.3)
23
+ dry-cli (1.4.1)
24
+ ed25519 (1.4.0)
25
+ erb (6.0.4)
26
+ io-console (0.8.2)
27
+ irb (1.18.0)
28
+ pp (>= 0.6.0)
29
+ prism (>= 1.3.0)
30
+ rdoc (>= 4.0.0)
31
+ reline (>= 0.4.2)
32
+ logger (1.7.0)
33
+ minitest (6.0.6)
34
+ drb (~> 2.0)
35
+ prism (~> 1.5)
36
+ minitest-profile (0.0.2)
37
+ minitest-reporters (1.8.0)
38
+ ansi
39
+ builder
40
+ minitest (>= 5.0, < 7)
41
+ ruby-progressbar
42
+ net-scp (4.1.0)
43
+ net-ssh (>= 2.6.5, < 8.0.0)
44
+ net-ssh (7.3.3)
45
+ pp (0.6.3)
46
+ prettyprint
47
+ prettyprint (0.2.0)
48
+ prism (1.9.0)
49
+ psych (5.4.0)
50
+ date
51
+ stringio
52
+ rake (13.4.2)
53
+ rdoc (7.2.0)
54
+ erb
55
+ psych (>= 4.0.0)
56
+ tsort
57
+ reline (0.6.3)
58
+ io-console (~> 0.5)
59
+ ruby-progressbar (1.13.0)
60
+ stringio (3.2.0)
61
+ tsort (0.2.0)
62
+
63
+ PLATFORMS
64
+ arm64-darwin-23
65
+ ruby
66
+
67
+ DEPENDENCIES
68
+ debug
69
+ mimas!
70
+ minitest
71
+ minitest-profile
72
+ minitest-reporters
73
+ rake
74
+
75
+ CHECKSUMS
76
+ ansi (1.6.0) sha256=ac9ea0c0ea8d32fb4e271348e609963ac78882f34b73836c2a02b3622e666658
77
+ bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6
78
+ builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
79
+ date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
80
+ debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
81
+ drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
82
+ dry-cli (1.4.1) sha256=b8015bb76c708aa8705a36faf694973e75eeeffca39b89c8e172dc6f66a7d874
83
+ ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506
84
+ erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9
85
+ io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
86
+ irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
87
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
88
+ mimas (0.1.0)
89
+ minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
90
+ minitest-profile (0.0.2) sha256=caf5306217b59ea2854d75ff6b312e2c139f2948f2b6a39486677b8317b7048e
91
+ minitest-reporters (1.8.0) sha256=8ce5280fb73ad3178ae525454df169b6f28c1b38b1d088ea91815d3a370ba384
92
+ net-scp (4.1.0) sha256=a99b0b92a1e5d360b0de4ffbf2dc0c91531502d3d4f56c28b0139a7c093d1a5d
93
+ net-ssh (7.3.3) sha256=831def58b2c51dcef66ec00d29397d4f210de89c19fe78f95873ca30f386e86a
94
+ pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
95
+ prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
96
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
97
+ psych (5.4.0) sha256=14f72d69a611af663d7d70e4a7b67d9eb1f3ae9f8d916b478961d5a0075ba5b7
98
+ rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
99
+ rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
100
+ reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
101
+ ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
102
+ stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
103
+ tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
104
+
105
+ BUNDLED WITH
106
+ 4.0.6
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026-present Ayush Newatia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,346 @@
1
+ # Mimas
2
+
3
+ Mimas is a CLI tool to deploy Ruby apps and static sites to a single server. It provides an opinionated setup to serve sites with [Caddy](https://caddyserver.com) as a reverse proxy.
4
+
5
+ It can provision the server by applying security rules, create the users, configure log rotation, and install required software.
6
+
7
+ Multiple sites can be deployed to the same server. All deploys are zero-downtime. Mimas will create a new `systemd` service for each deploy, healthcheck it, before switching Caddy to the new service, and stopping and removing the old service.
8
+
9
+ It doesn't do anything fancy under the hood, it's all the bash commands and scripts run by Ruby's `Net::SSH` utility.
10
+
11
+ Mimas focuses on deploying apps, and doesn't build the app during deployment. Build your app locally or on your CI server before pushing it using Mimas.
12
+
13
+ It has currently only been tested with Ubuntu 26.04. Mimas is still in early stages of development, so bugs are likely. Use with caution!
14
+
15
+ ## Getting Started
16
+
17
+ Install Mimas and create a binstub:
18
+
19
+ ```shell
20
+ $ bundle add mimas
21
+ $ bundle binstub mimas
22
+ ```
23
+
24
+ Run the `init` command to initialise a config file:
25
+
26
+ ```shell
27
+ $ bin/mimas init
28
+ ```
29
+
30
+ The command will ask you a number of questions about your setup and generate a config file for Mimas, as well as a [Caddyfile](https://caddyserver.com/docs/caddyfile).
31
+
32
+ ## Configuration
33
+
34
+ Mimas requires a config file located at `deploy/mimas.rb`. It is defined using a DSL as demonstrated below:
35
+
36
+ ```ruby
37
+ # A static site declaration. All shown fields are compulsory.
38
+ # You may pass a name as `site :static do ... end`. When no name
39
+ # is specified, it is set to `:default`.
40
+ site do
41
+ domain "<%= domain %>"
42
+ root_dir "output"
43
+ glob "**/*"
44
+ end
45
+
46
+ # A Ruby service declaration. For Ruby services, Use the `files` directive rather
47
+ # than `root_dir` and `glob` as shown above. Mimas will create a TAR archive
48
+ # of the array returned by `files` and push that to the server.
49
+ site :app do
50
+ domain "<%= domain %>"
51
+ files do
52
+ `git ls-files -z`.split("\x0")
53
+ .reject { it.match(%r!(bin|mimas.rb|Caddyfile)!) }
54
+ end
55
+
56
+ # Optional path to conduct the healthcheck. Defaults to "/".
57
+ healthcheck_path "/up"
58
+
59
+ # Define the console command for your app here.
60
+ # You can then access the console by running `mimas console SERVER_NAME`
61
+ # console "bundle console"
62
+
63
+ # Assign environment variables for your Ruby process. Return a multi-line string.
64
+ # env do
65
+ # <<~STRING
66
+ # STRING
67
+ # end
68
+
69
+ # Your deploy script. Mimas automatically runs `bundle install` on
70
+ # deploy if no deploy script is defined.
71
+ # Ensure you invoke `bundle install` if you provide your own script here.
72
+ # deploy do
73
+ # <<~STRING
74
+ # STRING
75
+ # end
76
+
77
+ # The services for your Ruby app. Each service will have its own
78
+ # `systemd` file.
79
+ services do
80
+ web do
81
+ start "bundle exec falcon host deploy/falcon.rb"
82
+ end
83
+ end
84
+ end
85
+
86
+ # A server declaration. You'll use the server's name as defined here
87
+ # to deploy to it. `host` and `user` are compulsory, but additional
88
+ # parameters for the SSH connection may be defined. They will be passed
89
+ # as-is to `Net::SSH`: https://net-ssh.github.io/ssh/v1/chapter-2.html#s3.
90
+ #
91
+ # `Net::SSH` automatically reads the private key based on your SSH config.
92
+ # More info: https://binarysolo.blog/managing-ssh-key-pairs-for-server-access/
93
+ server :production do
94
+ host "<%= ip %>"
95
+ user "deploy"
96
+ # keys []
97
+ end
98
+
99
+ # You can define optionally hooks that run before and after code is pushed
100
+ # to a server. You can use this to build your site locally before deployment.
101
+ # Mimas doesn't build the site on deploy.
102
+ # hooks do
103
+ # before_push do
104
+ # puts "This is a before push hook"
105
+ # end
106
+
107
+ # after_push do
108
+ # puts "This is an after push hook"
109
+ # end
110
+ # end
111
+ ```
112
+
113
+ You can define multiple sites in a single project as demonstrated above, you'll just need to `push` them independently. This approach can be useful if you're serving a static site and Ruby service from the same project.
114
+
115
+ ### Caddyfile
116
+
117
+ Mimas also requires a Caddyfile specific to your website. It will automatically load it into the main Caddyfile which controls the entire server. `bin/mimas init` will create one for you. It is an ERB template which is processed on each deploy.
118
+
119
+ ```
120
+ <%# An example Caddyfile.erb for a Ruby service %>
121
+ <%# In addition to the below variables, the `site` and `server` %>
122
+ <%# objects are also passed to it for additional configuration. %>
123
+ <%# See the source code for usage details on those objects. %>
124
+
125
+ <%= domain %> {
126
+ reverse_proxy {
127
+ to unix/<%= unix_socket %>
128
+
129
+ lb_try_duration 5s
130
+ }
131
+
132
+ log {
133
+ format console
134
+ output file <%= site_root %>/logs/caddy.log
135
+ }
136
+ }
137
+ ```
138
+
139
+ If you have multiple sites in your Mimas config, add the site name as an extension to the file name. For example, `Caddyfile.app.erb` will be used for a `site :app do ... end` in the Mimas config file.
140
+
141
+ See the [Caddy docs](https://caddyserver.com/docs/caddyfile-tutorial) for details on the Caddyfile.
142
+
143
+ ## Deployment
144
+
145
+ Once you have a valid Mimas config and a Caddyfile, you can provision your server:
146
+
147
+ ```shell
148
+ # bin/mimas provision SERVER_NAME
149
+ $ bin/mimas provision production
150
+ ```
151
+
152
+ This will harden your server with security rules, install all required software including Ruby and Caddy, and create an `admin` and `deploy` user.
153
+
154
+ Node is not installed as Mimas doesn't build sites during deployment, and is designed specifically to serve Ruby apps.
155
+
156
+ You can then push your website to the server:
157
+
158
+ ```shell
159
+ # bin/mimas push SERVER_NAME [SITE_NAME]
160
+ $ bin/mimas push production
161
+ ```
162
+
163
+ If nothing goes wrong, and your DNS is configured to point to your server, your site should be live at your domain name!
164
+
165
+ Mimas doesn't currently support rollbacks. Instead, I recommend you revert your code change and deploy again.
166
+
167
+ ## Mimas' Server Setup
168
+
169
+ Mimas creates two users on your server: `admin` and `deploy`. Root access and password authentication is disabled. You'll need to use SSH keys to authenticate.
170
+
171
+ The `admin` user has passwordless sudo access, and `deploy` does not. After the server is provisions, neither Mimas nor another automated tool should access the server using the `admin` account. All automated tasks should happen through the `deploy` account.
172
+
173
+ Mimas will install Caddy and set up a root `Caddyfile` in `/home/deploy/http`. This will import the Caddyfiles for all individual sites. The `deploy` user is given specific sudo access to the commands to control the Caddy systemd service.
174
+
175
+ Ruby versions are installed and managed using `ruby-install` and `chruby`. Ruby is built from source so installing a new version of Ruby on your server will take time.
176
+
177
+ Each site is contained in a folder under `/home/deploy/http` named after its domain name. Here's what the file structure looks like:
178
+
179
+ ```
180
+ /home/deploy/
181
+ └── http
182
+    └── example.com
183
+ ├── Caddyfile
184
+    ├── current -> /home/deploy/http/example.com/releases/20260721194912
185
+    ├── logs
186
+    ├── mimas
187
+    └── releases
188
+    ├── 20260721183301
189
+    ├── 20260721193851
190
+    └── 20260721194912
191
+
192
+ ```
193
+
194
+ On each deploy, a new folder is created under `releases` with the current timestamp. The `current` symlink will always point to the currently live release, and only the last 5 releases are retained. Older releases are deleted on deploy.
195
+
196
+ The `mimas` folder contains a `mimas.env` file which is loaded for each service, and the Unix socket for the Ruby app server.
197
+
198
+ On each deploy, Mimas will create a user systemd service in `~/.config/systemd/user/` and start it. If the healthcheck succeeds, it will point Caddy to the new release and reload it, then stop and delete the old service. This switch should be seamless, without downtime, as long as your application server supports graceful stops without dropping requests (both Puma and Falcon do).
199
+
200
+ ## Configuring The App Server
201
+
202
+ Mimas requires your Ruby app to run on a Unix socket. The `MIMAS_UNIX_SOCKET` environment variable defines the path to the socket. Read that into your app server's config and bind it to that path. A new socket is created for each release to avoid clashes so both old and new services can run at the same time.
203
+
204
+ Mimas will create configs for Puma or Falcon when you run `bin/mimas init`. Ensure you read the docs for your server to configure it for production use. The stock configs only contain the settings to bind it to the correct socket, and options like worker and thread count are not set.
205
+
206
+ ## How It Works
207
+
208
+ During provisioning, Mimas will install Caddy using the `admin` account and create a systemd service for it which runs under the `deploy` user. The `deploy` user is given specific sudo access only to control this server.
209
+
210
+ A global Caddyfile is created under `/home/deploy/http` which looks like:
211
+
212
+ ```
213
+ import caddy.global
214
+ import */Caddyfile
215
+ ```
216
+
217
+ `caddy.global` contains settings that apply to all sites. The default configures it to run using HTTP/1.1 and HTTP/2 only. I found HTTP/3 causes weird connection issues in a browser between deploys.
218
+
219
+ The second line automatically imports all site-specific Caddyfiles whenever Caddy is reloaded. This way, you can control your site's config independently.
220
+
221
+ ### Static Sites
222
+
223
+ When a static site is deployed, Mimas will create an array of all the output files using the `root_dir` and `glob` directives in the config, and then create a TAR archive of the files. The archive is SCP'd to the server and extracted under a new `releases` folder named after the current timestamp. It will then update the `current` symlink to the latest release.
224
+
225
+ Mimas will not build your site on the server. I believe the app server should not be used as a build server. Use a `before_push` hook to build your site locally or on a CI server.
226
+
227
+ ### Ruby Apps
228
+
229
+ To deploy Ruby apps, Mimas will use the the `files` directive in the config to generate an array of files and create a TAR archive. It will then read the `.ruby-version` file, or if one doesn't exist, the `RUBY_VERSION` constant and verify whether that is installed on the server. If it isn't, the deploy will stop there. Install the required version with `bin/mimas install ruby VERSION SERVER_NAME`
230
+
231
+ If the correct Ruby version is installed, deployment will continue as:
232
+
233
+ 1. SCP the archive to the server and extract it to a new `releases` folder named after the current timestamp.
234
+ 2. Create a new systemd service for the release and start it.
235
+ 3. Perform a healthcheck by making a `curl` request to the new release's Unix socket.
236
+ 4. Switch the `current` symlink to the new release and reload Caddy.
237
+ 5. Gracefully stop the old service and delete it.
238
+
239
+ If deployment fails at any step, all newly released files and services will be cleaned up, and the existing version will continue to be served.
240
+
241
+ ## Logs
242
+
243
+ Logs are written to the site's `logs/` folder. For systemd services, the `stdout` and `stderr` streams are both directed to a log file named after the service.
244
+
245
+ Log rotation is automatically configured for this folder.
246
+
247
+ ## CLI
248
+
249
+ Run the below command for all CLI options:
250
+
251
+ ```shell
252
+ $ bin/mimas help
253
+ ```
254
+
255
+ ## Incomplete Functionality
256
+
257
+ Mimas creates a `.env` file in the root directory of each site, which is read into the systemd service. There is no built-in way as yet to add content to this file. You'll need to SSH to your server, edit the file, and then re-deploy to pick up the changes. It lives across releases so you won't lose your changes on deploy.
258
+
259
+ You can, however, define environment variables in your Mimas config using `env`. These are written to `<site_root>/mimas/mimas.env`.
260
+
261
+ The purpose of the `.env` file is to store sensitive secrets like API keys that shouldn't be stored in the project. I'm still not completely sure of how to use the file as yet, so it exists but needs manual editing.
262
+
263
+ ## Plugins
264
+
265
+ Mimas supports plugins that add or override commands, or add hooks. Common use cases would be to override the `init` command to generate a framework-specific config file, or to add hooks specific to your project or framework.
266
+
267
+ Plugins must advertise that they are Mimas plugins in their gemspec:
268
+
269
+ ```ruby
270
+ spec.metadata[:mimas_plugin] = "true"
271
+ ```
272
+
273
+ Register your plugin as shown below:
274
+
275
+ ```ruby
276
+ Mimas::Plugins.register(Mimas::Plugins::Bridgetown)
277
+ ```
278
+
279
+ Here, `Mimas::Plugins::Bridgetown` must be an object (usually a module) that responds to the class methods `hooks` and `commands`
280
+
281
+ The `hooks` method must return a `Mimas::Hooks` object:
282
+
283
+ ```ruby
284
+ def hooks
285
+ hooks = Mimas::Hooks.new
286
+
287
+ hooks.before_push do
288
+ `bin/bt deploy`
289
+ end
290
+
291
+ hooks
292
+ end
293
+ ```
294
+
295
+ The `commands` method must return a Hash mapping the command name to the class containing it's implementation:
296
+
297
+ ```ruby
298
+ def commands
299
+ { "init" => Commands::Init }
300
+ end
301
+ ```
302
+
303
+ The CLI is built on `dry-cli`, so your commands must be structured as per that framework.
304
+
305
+ ## Design Decisions
306
+
307
+ In this section I'll briefly discuss why I made certain choices for Mimas.
308
+
309
+ ### CLI Framework
310
+
311
+ I decided on `dry-cli` as it has an elegant API and allows for easy extensibility. Also, the scope is fairly small so it's easy to follow the source code.
312
+
313
+ ### Caddy
314
+
315
+ Anyone who's ever used Nginx knows that hitting your head against the nearest wall is a more productive endeavour than dealing with an Nginx configuration. Caddy has fantastic docs and an easy to read configuration. Anything you could possibly need out of a web server and reverse proxy, Caddy has out of the box.
316
+
317
+ I considered a setup where Falcon was internet-facing and used for SSL termination and SNI resolution to serve one or more apps. It can be started as `falcon virtual` for this very reason. However, Falcon is an app server and not a proxy or web server so it is limiting in what you can do. Also, it would make zero-downtime deploys when upgrading Ruby impossible, as it spawn child processes for each site from the main process.
318
+
319
+ Caddy is the right tool for the job and allows us to manage the proxy independently from the sites and their individual Ruby requirements.
320
+
321
+ ### Docker
322
+
323
+ Docker's a bit of a behemoth. It's great when you're deploying to a fleet of servers, but to deploy to a single server, I find it a but excessive. It's also one more tool to understand and learn. Without Docker, there's a few other complexities around managing dependencies, but generally, I feel it's the simpler setup.
324
+
325
+ ### The `deploy` User
326
+
327
+ I'm not comfortable with the app or any automated tool having access to an account with `sudo` access. As such I designed the setup so we can do everything we need through the `deploy` user without `sudo` access.
328
+
329
+ ## Testing
330
+
331
+ * Run `bundle exec rake test` to run the test suite
332
+
333
+ ## Contributing
334
+
335
+ 1. Fork it (https://codeberg.org/ayushn21/mimas/fork)
336
+ 2. Clone the fork using `git clone` to your local development machine.
337
+ 3. Create your feature branch (`git checkout -b my-new-feature`)
338
+ 4. Commit your changes (`git commit -am 'Add some feature'`)
339
+ 5. Push to the branch (`git push origin my-new-feature`)
340
+ 6. Create a new Pull Request
341
+
342
+ ## License
343
+
344
+ Mimas is released under the [MIT License](https://opensource.org/licenses/MIT).
345
+
346
+ Copyright © 2026 [Ayush Newatia](https://radioactivetoy.tech)
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+ require "minitest/test_task"
3
+
4
+ Minitest::TestTask.create
data/bin/mimas ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/mimas"
4
+
5
+ Dry::CLI.new(Mimas::CLI::Commands).call
@@ -0,0 +1,68 @@
1
+ require 'zlib'
2
+ require 'rubygems/package'
3
+
4
+ module Mimas
5
+ module Archive
6
+ def archive(root: Dir.pwd, output:, archive_root: "", glob: nil, files: [])
7
+ Archiver
8
+ .new(root: root, output: output, archive_root: archive_root, glob: glob, files: files)
9
+ .package
10
+ end
11
+ end
12
+
13
+ class Archiver
14
+ def initialize(root:, output:, archive_root:, glob: nil, files: [])
15
+ @root = root || Dir.pwd
16
+ @output = output
17
+ @archive_root = archive_root || ""
18
+ @glob = glob
19
+ @source_files = files
20
+ end
21
+
22
+ def package
23
+ gz = Zlib::GzipWriter.open(@output)
24
+ tar = Gem::Package::TarWriter.new(gz)
25
+
26
+ files.each { add_file(tar, it) }
27
+
28
+ tar.close
29
+ gz.close
30
+ end
31
+
32
+ private
33
+ def files
34
+ @files ||=
35
+ if @glob
36
+ in_dir(@root) { Dir.glob(@glob) }
37
+ else
38
+ @source_files
39
+ end
40
+ end
41
+
42
+ def add_file(tar, path)
43
+ source_path = File.expand_path(path, @root)
44
+ destination_path = File.join(@archive_root, path)
45
+
46
+ stat = File.lstat(source_path)
47
+
48
+ if stat.directory?
49
+ tar.mkdir(destination_path, stat.mode)
50
+ else
51
+ tar.add_file_simple(destination_path, stat.mode, stat.size) do |io|
52
+ File.open(source_path, "rb") do |source_file|
53
+ IO.copy_stream(source_file, io)
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def in_dir(dir, &block)
60
+ initial_dir = Dir.pwd
61
+ Dir.chdir(dir)
62
+ return_value = yield
63
+ Dir.chdir(initial_dir)
64
+
65
+ return return_value
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,22 @@
1
+ module Mimas
2
+ module CLI
3
+ module Commands
4
+ class BaseCommand < Dry::CLI::Command
5
+ include Terminal::Printer, SSH, Template
6
+
7
+ def initialize
8
+ super
9
+
10
+ if Config.current
11
+ @hooks = Hooks.new
12
+ @hooks.merge(Config.current.hooks)
13
+
14
+ Plugins.fetch.each do |plugin|
15
+ @hooks.merge plugin.hooks if plugin.respond_to?(:hooks)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
data/lib/mimas/cli.rb ADDED
@@ -0,0 +1,22 @@
1
+ module Mimas
2
+ module CLI
3
+ module Commands
4
+ extend Dry::CLI::Registry
5
+
6
+ register "init", Init
7
+ register "install ruby", Install
8
+ register "provision", Provision
9
+ register "push", Push
10
+ register "console", Console
11
+ register "setup caddy", Setup::Caddy
12
+ register "setup deploy_user", Setup::DeployUser
13
+ register "setup ruby", Setup::Ruby
14
+ end
15
+
16
+ Plugins.fetch.each do |plugin|
17
+ plugin.commands.each do |command, target|
18
+ CLI::Commands.register(command.to_s, target)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ module Mimas
2
+ module CLI
3
+ module Commands
4
+ class Console < BaseCommand
5
+ desc "Run your app's console"
6
+
7
+ argument :server_name, required: true, desc: "The name of the server to run the console on"
8
+ argument :site, desc: "The site to run the console for. Uses the default site if unspecified"
9
+
10
+ def call(server_name, site: :default)
11
+ server = Config.current.servers[server_name.to_sym]
12
+ site = Config.current.sites[site.to_sym]
13
+
14
+ raise "Please add a `console` directive to your config file" unless site.respond_to?(:console)
15
+
16
+ server.start_console(site)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end