standard-procedure-anvil 0.1.6 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ce4bfd700a5d2b870087b7d983902c43bc377ebcba1abbb911caea507aba0e8
4
- data.tar.gz: 4e902474667b96efaf5a86435b381f6e54f28d58a1ae6a216441d9de6eb35925
3
+ metadata.gz: 7330f81fa4611c93e7bd6424be609a05da36a04e0372c8401fdc58a3e1b09afb
4
+ data.tar.gz: 7f19466d160effbd3050ae33d1273487ccfd55e87f22575b75e578b6c3798b17
5
5
  SHA512:
6
- metadata.gz: 6a6c16d63f3b0c2bbbafcce079bdb7ba7ea8b44f2c5e0f4ac0f5a9beb3e1f2999852be11aeaed5c107bb88eaef1b7a09b0bf3fef4eaeabb9f5760c625f73c845
7
- data.tar.gz: ca793262284e8938121d155fe18f0b6c527a50b9bf0bf437f9a3973d02d708060c317ece52f85713913ec0995937a88eb93d58af85293d01abcf50f5005d5493
6
+ metadata.gz: 0e0c452fab0076406770c3cff17458451cfe4ff58ae69e4fdc456b474f38cd1b7f9a5cf8cd3f619b0f6be8ac426e3ee7b618992b6f91de3a6400fa4d849363c4
7
+ data.tar.gz: b42162b6f694f90d4ab33b4734dbdfa465fd506998ddf687ec49b48142dd590021ad1af37d3518cc781415ff77585f7979449dc4f4a6754ca5fb9e152090163a
data/CHANGELOG.md CHANGED
@@ -3,3 +3,17 @@
3
3
  ## [0.1.0] - 2023-06-19
4
4
 
5
5
  - Initial release
6
+
7
+ ## [0.2.0] - 2023-07-05
8
+
9
+ - It works for me
10
+ Successfully deployed a number of apps into production using this
11
+
12
+ ## [0.2.1] - 2023-07-06
13
+
14
+ - Corrected dokku proxy SSL settings
15
+ - Tidy up of various bits of code and configuration
16
+
17
+ ## [0.2.2] - 2023-08-14
18
+
19
+ - Updated the redis cloudinit file to use the latest version from Redis, instead of the older one that is included with Ubuntu.
data/README.md CHANGED
@@ -2,136 +2,44 @@
2
2
 
3
3
  Some simple scripts for installing [Dokku](https://dokku.com) applications on Ubuntu servers.
4
4
 
5
+ ## Why does this exist?
6
+
7
+ I needed a tool to [simplify the management](/docs/why.md) of my many dokku-deployed Ruby on Rails apps.
8
+
5
9
  ## Installation
6
10
 
11
+ Anvil requires Ruby 2.7 or newer, as it uses ConcurrentRuby to handle doing more than one thing at once.
12
+
7
13
  ```ruby
8
14
  gem install standard-procedure-anvil
9
15
  ```
10
16
 
11
17
  ## Usage
12
18
 
13
- ### To build a new server
14
-
15
- Coming soon (plan is to use [Fog](https://github.com/fog/fog) to handle building servers)
16
-
17
- ### To install an application onto a blank server
18
-
19
- Move to your application's root folder and create the anvil.yml file (see below).
20
-
21
- Then run `anvil host --user=user --use-sudo --identity=~/.ssh/my_key`.
22
-
23
- This will SSH into each server and:
24
-
25
- - Sets the server hostname and timezone
26
- - Installs various necessary packages, plus dokku itself
27
- - Sets up the firewall
28
- - Creates unix users for each app, adding them to the sudo and docker groups, and setting their authorized_keys files with the given public key
29
- - Schedule a `docker system prune` once per week to clean up any dangling images or containers
30
- - Configure nginx
31
- - Install dokku plugins and run any configuration you have defined
32
- - Sets the dokku deployment branch to `main`
33
- - Disallows root and passwordless logins over SSH
34
-
35
- For each app it will then:
36
-
37
- - Create the dokku app
38
- - Set the environment variables to those defined in your configuration and secrets files
39
- - Set the app's domain and set up a proxy from nginx to the app's port
40
- - Sets resource limits for the app
41
- - Disables checks for workers
42
-
43
- Then a git remote for each app is created on your local machine and then pushed. This performs the initial dokku deployment. Once complete:
44
-
45
- - The app is scaled to the correct number of workers
46
- - Plugins are configured for the app
47
-
48
- ### Configuration Files
49
-
50
- An Anvil configuration file specifies the configuration for multiple servers and multiple apps. Each server is configured, then each app is installed onto each server.
51
-
52
- So you could have one app on two servers (and, we assume, a load-balancer set up in front of them). Or two apps on one server. Or even two apps on two servers (again, using a load-balancer)
53
-
54
-
55
- ```yml
56
- version: 0.1
57
- servers:
58
- hosts:
59
- - server1.example.com
60
- user: user
61
- public_key: /home/local-user/.ssh/my-key.pub
62
- timezone: Europe/London
63
- ports:
64
- - 22/tcp
65
- - 80/tcp
66
- - 443/tcp
67
- nginx:
68
- forward_proxy_headers: false
69
- client_max_body_size: 512m
70
- proxy_read_timeout: 60s
71
- plugins:
72
- cron-restart:
73
- url: https://github.com/dokku/dokku-cron-restart.git
74
- maintenance:
75
- url: https://github.com/dokku/dokku-maintenance.git
76
- redis:
77
- url: https://github.com/dokku/dokku-redis.git
78
- memcached:
79
- url: https://github.com/dokku/dokku-memcached.git
80
- letsencrypt:
81
- url: https://github.com/dokku/dokku-letsencrypt.git
82
- config:
83
- - set --global email ssl-admin@mycompany.com
84
- - cronjob --add
85
- apps:
86
- first_app:
87
- hostname: first_app.example.com
88
- port: 3000
89
- environment:
90
- - ENV_VAR=value
91
- - ENV_VAR2=value2
92
- - RAILS_ENV=production
93
- secrets: secrets.yml
94
- resource_limit: 2048m
95
- scale: web=2 worker=1
96
- plugins:
97
- cron-restart:
98
- - set first_app schedule '0 3 * * *'
99
- redis:
100
- - create first_app_redis_db
101
- - link first_app_redis_db first_app
102
- memcached:
103
- - create first_app_memcached
104
- - link first_app_memcached first_app
105
- letsencrypt:
106
- - set first_app email ssl-admin@mycompany.com
107
- - enable first_app
108
- second_app:
109
- hostname: second_app.example.com
110
- port: 3000
111
- environment:
112
- - ENV_VAR=value
113
- - ENV_VAR2=value2
114
- - RAILS_ENV=production
115
- secrets: secrets.yml
116
- resource_limit: 2048m
117
- scale: web=2 worker=1
118
- plugins:
119
- cron-restart:
120
- - set second_app schedule '0 3 * * *'
121
- letsencrypt:
122
- - set second_app email ssl-admin@mycompany.com
123
- - enable second_app
124
- ```
125
- `secrets.yml` is an optional additional file containing environment variables that you do not want to check into your source code repository. It is a simple KEY=VALUE format:
19
+ ### Build a server
126
20
 
127
- ```
128
- DB_PASSWORD=letmein
129
- ENCRYPTION_KEY=secretstuff
130
- ```
21
+ Ultimately the plan is to use [Fog](https://github.com/fog/fog) to handle building servers.
22
+
23
+ But until then, you can prepare your servers using [CloudInit](https://cloudinit.readthedocs.io/en/latest/)
131
24
 
25
+ [Generating a cloudinit file](/docs/cloudinit.md) with `anvil cloudinit generate`
26
+
27
+ ### Install and deploy
28
+
29
+ Use the `anvil app install` and `anvil app deploy` commands to [install and deploy](/docs/app.md) your app to your server.
30
+
31
+ ### Manage and reconfigure
32
+
33
+ Use `anvil app scale` and `anvil app reconfigure` to manage and reconfigure your app. (Docs coming soon)
34
+
35
+ ### Ruby on Rails
36
+
37
+ I'm a Rails developer and I built anvil to help me with my Rails apps. Here are [some things I learnt along the way](/docs/ruby-on-rails.md).
132
38
 
133
39
  ## Contributing
134
40
 
41
+ Check out the [Roadmap](/docs/roadmap.md)
42
+
135
43
  Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/standard-procedure-anvil.
136
44
 
137
45
  ## License
@@ -11,11 +11,24 @@ packages:
11
11
  - ufw
12
12
  - wget
13
13
  - apt-transport-https
14
+ - ca-certificates
15
+ - curl
16
+ - gpg-agent
17
+ - software-properties-common
14
18
  package_update: true
15
19
  package_upgrade: true
16
20
  runcmd:
17
21
  # General server setup
18
22
  - timedatectl set-timezone UTC
23
+ - echo "${USER}:${USER}" | chpasswd
24
+ # Prepare for Docker
25
+ - sudo install -m 0755 -d /etc/apt/keyrings
26
+ - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
27
+ - sudo chmod a+r /etc/apt/keyrings/docker.gpg
28
+ - echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
29
+ # Install docker
30
+ - apt-get update
31
+ - apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
19
32
  # Fail2Ban setup
20
33
  - printf "[sshd]\nenabled = true\nbanaction = iptables-multiport" > /etc/fail2ban/jail.local
21
34
  - systemctl enable fail2ban
@@ -11,35 +11,29 @@ packages:
11
11
  - ufw
12
12
  - wget
13
13
  - apt-transport-https
14
- - mysql-client
15
- - libmysqlclient-dev
14
+ - ca-certificates
15
+ - curl
16
+ - gpg-agent
17
+ - software-properties-common
16
18
  package_update: true
17
19
  package_upgrade: true
18
20
  runcmd:
19
21
  # General server setup
20
22
  - timedatectl set-timezone UTC
21
- # Install MySQL
22
- - echo "mysql-server mysql-server/root_password password root" | sudo debconf-set-selections
23
- - echo "mysql-server mysql-server/root_password_again password root" | sudo debconf-set-selections
24
- - sudo apt-get -y install mysql-server
25
- - |
26
- cat >> /etc/mysql/mysql.conf.d/utf8.cnf << CONF
27
- [client]
28
- default-character-set=utf8mb4
29
-
30
- [mysql]
31
- default-character-set=utf8mb4
32
-
33
- [mysqld]
34
- init_connect='SET collation_connection = utf8mb4_unicode_ci'
35
- init_connect='SET NAMES utf8mb4'
36
- character-set-server=utf8mb4
37
- collation-server=utf8mb4_unicode_ci
38
- skip-character-set-client-handshake
39
- CONF
40
- - sed -i -e '/^\(#\|\)bind-address/s/^.*$/bind-address = 0.0.0.0/' /etc/mysql/mysql.conf.d/mysqld.cnf
41
- # Start MySQL
42
- - systemctl start mysql.service
23
+ - echo "${USER}:${USER}" | chpasswd
24
+ # Prepare for Docker
25
+ - sudo install -m 0755 -d /etc/apt/keyrings
26
+ - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
27
+ - sudo chmod a+r /etc/apt/keyrings/docker.gpg
28
+ - echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
29
+ # Install docker
30
+ - apt-get update
31
+ - apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
32
+ # Install dokku
33
+ - echo "dokku dokku/vhost_enable boolean true" | sudo debconf-set-selections
34
+ - wget https://dokku.com/install/v0.30.7/bootstrap.sh && sudo DOKKU_TAG=v0.30.7 bash bootstrap.sh
35
+ - cat /home/app/.ssh/authorized_keys | dokku ssh-keys:add admin
36
+ - dokku git:set --global deploy-branch main
43
37
  # Fail2Ban setup
44
38
  - printf "[sshd]\nenabled = true\nbanaction = iptables-multiport" > /etc/fail2ban/jail.local
45
39
  - systemctl enable fail2ban
@@ -59,3 +53,4 @@ runcmd:
59
53
  - sed -i '$a AllowUsers %{USER} dokku' /etc/ssh/sshd_config
60
54
  # And we're done
61
55
  - reboot
56
+
@@ -10,13 +10,13 @@ packages:
10
10
  - fail2ban
11
11
  - ufw
12
12
  - wget
13
- - memcached
14
13
  - logrotate
15
14
  package_update: true
16
15
  package_upgrade: true
17
16
  runcmd:
18
17
  # General server setup
19
18
  - timedatectl set-timezone UTC
19
+ - echo "${USER}:${USER}" | chpasswd
20
20
  # Fail2Ban setup
21
21
  - printf "[sshd]\nenabled = true\nbanaction = iptables-multiport" > /etc/fail2ban/jail.local
22
22
  - systemctl enable fail2ban
@@ -34,6 +34,7 @@ runcmd:
34
34
  - sed -i -e '/^\(#\|\)AuthorizedKeysFile/s/^.*$/AuthorizedKeysFile .ssh\/authorized_keys/' /etc/ssh/sshd_config
35
35
  - sed -i '$a AllowUsers %{USER}' /etc/ssh/sshd_config
36
36
  # Set up memcached
37
+ - apt-get -y install memcached
37
38
  - sed -i 's/-m 64/-m 512/g' /etc/memcached.conf
38
39
  - sed -i 's/-l 127.0.0.1/-l 0.0.0.0/g' /etc/memcached.conf
39
40
  - systemctl restart memcached.service
@@ -16,10 +16,11 @@ package_upgrade: true
16
16
  runcmd:
17
17
  # General server setup
18
18
  - timedatectl set-timezone UTC
19
+ - echo "${USER}:${USER}" | chpasswd
19
20
  # Install MySQL
20
21
  - echo "mysql-server mysql-server/root_password password root" | sudo debconf-set-selections
21
22
  - echo "mysql-server mysql-server/root_password_again password root" | sudo debconf-set-selections
22
- - sudo apt-get -y install mysql-server
23
+ - apt-get -y install mysql-server
23
24
  - |
24
25
  cat >> /etc/mysql/mysql.conf.d/utf8.cnf << CONF
25
26
  [client]
@@ -10,14 +10,13 @@ packages:
10
10
  - fail2ban
11
11
  - ufw
12
12
  - wget
13
- - docker.io
14
- - docker-compose
15
13
  - apt-transport-https
16
14
  package_update: true
17
15
  package_upgrade: true
18
16
  runcmd:
19
17
  # General server setup
20
18
  - timedatectl set-timezone UTC
19
+ - echo "${USER}:${USER}" | chpasswd
21
20
  # Prepare for OpenSearch
22
21
  - swapoff -a
23
22
  - echo "vm.max_map_count=262144" > /etc/sysctl.d/98-opensearch.conf
@@ -39,6 +38,14 @@ runcmd:
39
38
  - sed -i -e '/^\(#\|\)AllowAgentForwarding/s/^.*$/AllowAgentForwarding no/' /etc/ssh/sshd_config
40
39
  - sed -i -e '/^\(#\|\)AuthorizedKeysFile/s/^.*$/AuthorizedKeysFile .ssh\/authorized_keys/' /etc/ssh/sshd_config
41
40
  - sed -i '$a AllowUsers %{USER}' /etc/ssh/sshd_config
41
+ # Install docker
42
+ - apt-get install -y ca-certificates curl gnupg
43
+ - install -m 0755 -d /etc/apt/keyrings
44
+ - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
45
+ - chmod a+r /etc/apt/keyrings/docker.gpg
46
+ - echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
47
+ - apt-get update
48
+ - apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
42
49
  # OpenSearch setup
43
50
  - mkdir -p /etc/opensearch
44
51
  - docker pull opensearchproject/opensearch:latest
@@ -55,7 +62,7 @@ runcmd:
55
62
  - node.name=search_db
56
63
  - bootstrap.memory_lock=true
57
64
  - plugins.security.disabled=true
58
- - "OPENSEARCH_JAVA_OPTS=-Xms4096m -Xmx4096m"
65
+ - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx3184m"
59
66
  ulimits:
60
67
  memlock:
61
68
  soft: -1
@@ -71,16 +78,18 @@ runcmd:
71
78
  volumes:
72
79
  opensearch_data:
73
80
  EOF
81
+ - cd /etc/opensearch && docker compose build
74
82
  - |
75
83
  cat >> /etc/systemd/system/opensearch.service << EOF
76
- Description=OpenSearch container
84
+ [Unit]
85
+ Description=OpenSearch
77
86
  Requires=docker.service
78
87
  After=docker.service
79
88
  [Service]
80
89
  WorkingDirectory=/etc/opensearch
81
90
  Restart=always
82
- ExecStart=/usr/bin/docker-compose up
83
- ExecStop=/usr/bin/docker-compose down
91
+ ExecStart=/usr/bin/docker compose up
92
+ ExecStop=/usr/bin/docker compose down
84
93
  [Install]
85
94
  WantedBy=multi-user.target
86
95
  EOF
@@ -10,13 +10,13 @@ packages:
10
10
  - fail2ban
11
11
  - ufw
12
12
  - wget
13
- - redis-server
14
13
  - logrotate
15
14
  package_update: true
16
15
  package_upgrade: true
17
16
  runcmd:
18
17
  # General server setup
19
18
  - timedatectl set-timezone UTC
19
+ - echo "${USER}:${USER}" | chpasswd
20
20
  # Fail2Ban setup
21
21
  - printf "[sshd]\nenabled = true\nbanaction = iptables-multiport" > /etc/fail2ban/jail.local
22
22
  - systemctl enable fail2ban
@@ -34,10 +34,14 @@ runcmd:
34
34
  - sed -i -e '/^\(#\|\)AuthorizedKeysFile/s/^.*$/AuthorizedKeysFile .ssh\/authorized_keys/' /etc/ssh/sshd_config
35
35
  - sed -i '$a AllowUsers %{USER}' /etc/ssh/sshd_config
36
36
  # Set up Redis
37
+ - curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
38
+ - echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/redis.list
39
+ - apt-get update
40
+ - apt-get -y install redis-server
37
41
  - sed -i 's/supervised no/supervised systemd/g' /etc/redis/redis.conf
38
42
  - sed -i 's/bind 127.0.0.1 ::1/# bind 127.0.0.1 ::1/g' /etc/redis/redis.conf
39
43
  - sed -i 's/protected-mode yes/protected-mode no/g' /etc/redis/redis.conf
40
- - systemctl restart redis.service
44
+ - systemctl restart redis-server.service
41
45
  - |
42
46
  cat > /etc/logrotate.d/redis-server << EOF
43
47
  /var/log/redis/redis-server*.log {
@@ -0,0 +1 @@
1
+ fb8d74684e40d48b82c6ad58503e52b035a76fe4f534a8fcb5ec043227af442035ef5f0d1a845cb8e8bebc3556bfcb4718fca26dcb53b4f816f76c1990f0c4d5
@@ -0,0 +1 @@
1
+ 5c4b010036ad155590705bf75b564c167513f342524abf34073080de20ccdf2b2c0ef4671f38dccb53781603f7373537436c8a6bbc7a43a5eb33c3fabbd1821d
@@ -0,0 +1 @@
1
+ 2b56322e96127aa43986eaa2de436eafd64c7d5260d61db1a13605d95bf0fc3b331f006c0b534d845c05e781659518f37d3f8aa17475e6e926675d5ea1985a50
data/docs/app.md ADDED
@@ -0,0 +1,51 @@
1
+ # The `app` command
2
+
3
+ ## Installing an application
4
+
5
+ Move to your application's root folder and create the deploy.yml file (see below). Then use the `app install` command to set dokku up for your first deployment.
6
+
7
+ ```sh
8
+ anvil app install
9
+ ```
10
+
11
+ This will SSH into the server (or servers if you have multiple) from your config file and:
12
+
13
+ - Installs any dokku plugins that you have specified
14
+ - Tells dokku to create the app
15
+ - Uses your config file to set the environment variables for the app
16
+ - Sets some sensible defaults for Nginx and makes sure it proxies correctly to your app
17
+ - Optionally forwards the correct SSL/TLS headers if your app is behind a load-balancer
18
+ - Finally it runs the post-installation scripts from your config file, which you can use to configure your plugins
19
+
20
+ ## Deploying an application
21
+
22
+ Next up we deploy the app.
23
+
24
+ ```sh
25
+ anvil app deploy
26
+ ```
27
+ As this is the first deployment, anvil will create git remotes for each host, then do the initial git push. If you have multiple servers configured, these should run in parallel (coming soon). Once each deployment has completed, anvil will SSH in, scale your app and run the post-first-deployment scripts.
28
+
29
+ You can then use the same `anvil app deploy` command to deploy the app again - but as it knows this isn't the first deployment (as it does not need to create the git remotes), it will run your post-deployment scripts (not post-first-deployment) each time.
30
+
31
+ To change the number of processes (as defined by your Procfile), you can set the `scale` key(s) in your config file and then call:
32
+
33
+ ```sh
34
+ anvil app scale
35
+ ```
36
+
37
+ (COMING SOON)
38
+ Finally, if you need to change the values of any environment variables, update your config file and use:
39
+
40
+ ```sh
41
+ anvil app configure
42
+ ```
43
+
44
+ ## Configuration files
45
+
46
+ The [anvil configuration file](/docs/configuration.md) is the heart of the system.
47
+
48
+ ## Secrets
49
+
50
+ You don't want to store your secrets (passwords, encryption keys) in your anvil configuration. Instead anvil can [read your secrets](/docs/secrets.md) from a separate file or from the command line.
51
+
data/docs/cloudinit.md ADDED
@@ -0,0 +1,33 @@
1
+ # Building a server
2
+
3
+ ## Cloudinit
4
+
5
+ A [CloudInit](https://cloudinit.readthedocs.io/en/latest/) file is a YML file that you load into a virtual machine while it is being created. As the server boots, uses the cloudinit configuration to install software and set itself up. With most cloud hosting providers, you will find an option for "user data", or something similar, on the "create a new server" page.
6
+
7
+ ### Generate a configuration
8
+
9
+ So firstly we ask Anvil which cloudinit configurations it has available:
10
+
11
+ ```sh
12
+ anvil cloudinit list
13
+ ```
14
+
15
+ This will give us a list of prewritten cloud init scripts - of which dokku is probably the one we're most interested in.
16
+
17
+ Next we tell anvil to generate our configuration:
18
+
19
+ ```sh
20
+ anvil cloudinit generate dokku --user app --public-key ~/.ssh/my_key.pub > ~/Desktop/my_server.yml
21
+ ```
22
+
23
+ Anvil generates a dokku configuration (and places it on our desktop) that will create an Ubuntu 22.04 box with docker and dokku preinstalled. Plus it will create a user called `app` that can log in through SSH using a public key `my_key.pub`. The server itself is locked down so only ports 80, 443 and 22 are open, only the users `app` and `dokku` are allowed to log in and they must use public/private key encryption - no passwords allowed.
24
+
25
+ ### Testing your configuration
26
+
27
+ To test this, it's worth taking a look at [Multipass](https://multipass.run) - a tool from Canonical that lets you create virtual machines (using cloud init files) on your local machine. This means you can try out various configurations without spending money at a hosting company.
28
+
29
+ One thing to note when using multipass - it requires SSH access for a user called "ubuntu". So take your generated cloudinit file, locate the SSH configuration section and the "AllowUsers" line - and add the "ubuntu" user to it. Something like: ` - sed -i '$a AllowUsers %{USER} ubuntu dokku' /etc/ssh/sshd_config`.
30
+
31
+ Multipass has its own private key generated for the ubuntu user, and uses this to manage the server. Of course, the multipass VM is only on your machine, plus its private key is hidden away, so it's not a security risk. But in general `anvil cloudinit generate` disallows all SSH access apart from your named user (using your own key), and the `dokku` user if applicable.
32
+
33
+ Once you've built a preconfigured virtual machine, we can move on to getting our dokku application installed. However, note that it can take several minutes for the initialisation process to complete - so don't start your deployment too early, or your server won't be ready and will reboot whilst your setting things up.
@@ -0,0 +1,7 @@
1
+ # Anvil configuration files
2
+
3
+ An Anvil configuration file specifies the configuration for multiple servers and multiple apps. Each server is configured, then each app is installed onto each server.
4
+
5
+ For now take a look at the two samples in the spec folder - [single-server](/spec/fixtures/single-server.config.yml) and [multi-server](/spec/fixtures/multi-server.config.yml).
6
+
7
+ Also check out the [tips for Ruby on Rails](/docs/ruby-on-rails.md) which has an example configuration file.
data/docs/roadmap.md ADDED
@@ -0,0 +1,12 @@
1
+ # Roadmap
2
+
3
+ As I mentioned, this is pretty much designed for my own use.
4
+
5
+ There are a few bits I still need (which will be V1) and then I want to open it up. But if it gets too generic, you might as well just write a load of shell scripts to manage dokku yourself - it's important to keep it simple.
6
+
7
+ ## To do
8
+
9
+ - [ ] `app reconfigure`
10
+ - [ ] Add `--first`/`--not-first` options to `app deploy` so you can override the first-deployment behaviour (in case you get a failure and need to re-run everything)
11
+ - [ ] Instead of relying on the ssh-agent, allow the use of your private key when connecting to servers
12
+ - [ ] Parallel execution across multiple hosts
@@ -0,0 +1,69 @@
1
+ # Dokku and Ruby on Rails
2
+
3
+ (incomplete - coming soon)
4
+
5
+ - If using the Mysql plugin, use the Mysql2 protocol
6
+ - store your RAILS_MASTER_KEY and SECRET_KEY_BASE outside of your configuration file (see [secrets](/docs/secrets.md))
7
+ - in your config/environments/production.rb or config/environments/staging.rb set `config.force_ssl = false` - dokku's nginx configuration will do the redirect for you if you're using the Let's Encrypt plugin, or you can set the redirect on your load-balancer. Setting `config.force_ssl = true` causes issues with the health checks
8
+ - Make sure your app knows its hostname; set it as an environment variable in your configuration file and then use that hostname in your environment file as follows: `config.action_mailer.default_url_options = {host: ENV["HOSTNAME"]}` and `Rails.application.routes.default_url_options[:host] = config.action_mailer.default_url_options[:host]`. This means that when you need to generate a full URL (as opposed to a relative path), Rails knows what to use.
9
+ - Use a CHECKS file that looks like this (again using that HOSTNAME environment variable), so dokku's zero-deployment checks can connect correctly. My `/health_check` route just returns a `200 OK` in most apps, but in some it actually checks the database connection, as well as some other services (although this causes problems with the initial deployment, which is why checks are switched off the first time through)
10
+ ```
11
+ WAIT=10
12
+ ATTEMPTS=5
13
+ http://{{ var "HOSTNAME" }}/health_check
14
+ ```
15
+
16
+ A typical Rails deploy.yml for a single-server, totally self-contained app, looks like this:
17
+
18
+ ```yaml
19
+ version: 0.1
20
+ hosts:
21
+ - myapp.example.com:
22
+ user: app
23
+ app:
24
+ domain: myapp.example.com
25
+ port: 3000
26
+ environment:
27
+ - BUNDLE_WITHOUT=test:development
28
+ - CABLE_CHANNEL_PREFIX=myapp
29
+ - EMAIL_DOMAIN=mail.myapp.example.com
30
+ - EMAIL_HOST=smtp.myapp.example.com
31
+ - EMAIL_PORT=587
32
+ - EMAIL_USER=postmaster@mail.myapp.example.com
33
+ - HOSTNAME=myapp.example.com
34
+ - NODE_ENV=production
35
+ - RACK_ENV=production
36
+ - RAILS_ENV=production
37
+ - RAILS_LOG_TO_STDOUT=true
38
+ - RAILS_MAX_THREADS=10
39
+ - RAILS_SERVE_STATIC_FILES=true
40
+ resource_limit: 2048m
41
+ scale: web=2 worker=1
42
+ load_balancer: false
43
+ nginx:
44
+ client_max_body_size: 512m
45
+ proxy_read_timeout: 60s
46
+ plugins:
47
+ - cron-restart
48
+ - maintenance
49
+ - redis
50
+ - memcached
51
+ - mysql
52
+ - letsencrypt
53
+ scripts:
54
+ after_install:
55
+ - dokku cron-restart:set app schedule '0 3 * * *'
56
+ - dokku memcached:create memcached
57
+ - dokku memcached:link memcached app
58
+ - dokku redis:create redis_db
59
+ - dokku redis:link redis_db app
60
+ - dokku mysql:create mysql_db
61
+ - dokku config:set app MYSQL_DATABASE_SCHEME=mysql2
62
+ - dokku mysql:link mysql_db app
63
+ after_first_deploy:
64
+ - dokku letsencrypt:set app email me@myapp.example.com
65
+ - dokku letsencrypt:enable app
66
+ - dokku letsencrypt:cron-job --add
67
+ - dokku run app bin/rails db:seed
68
+
69
+ ```
data/docs/secrets.md ADDED
@@ -0,0 +1,20 @@
1
+ # Secrets
2
+
3
+ Finally, you'll probably want to check your deploy.yml file into source control. But you _definitely_ don't want to be storing important secrets - database passwords, encryption keys and so on - where everyone can see them.
4
+
5
+ So the `anvil app` commands also allow you to specify secrets, either from another file, or via the command line.
6
+
7
+ The secrets are just extra environment variables that are added to the ones defined in your config file - in the format:
8
+
9
+ ```
10
+ SECRET1=VALUE1 SECRET2=VALUE2
11
+ ```
12
+
13
+ You can either specify `--secrets my-secrets-file.env` to load these from a separate file. Or you can load them from stdin.
14
+
15
+ For example, I use [Bitwarden](https://bitwarden.com) as my password locker and use the Bitwarden CLI to access my secrets. The CLI is installed through homebrew, I then authenticate and can use a command like:
16
+
17
+ ```sh
18
+ bw get notes secrets@myapp.com | anvil app install deploy.myapp.yml -S
19
+ ```
20
+ I have the environment variables for myapp.com stored in Bitwarden as a secure note with the title "secrets@myapp.com". So `bw get notes secrets@myapp.com` loads them from my vault and pipes them to the `anvil app install` command. The anvil command is using the `-S` (or `--secrets-stdin`) option which means it will read the information piped in by bitwarden. So, once decrypted, the confidential data never touches a disk until it gets written into the dokku app configuration on the server.
data/docs/why.md ADDED
@@ -0,0 +1,17 @@
1
+ # Why does this exist?
2
+
3
+ [Dokku](https://dokku.com) is great at installing and configuring containers on a single host.
4
+
5
+ But you do need to install dokku, generate your configuration and environment variables, install your plugins and app and then configure all those plugins.
6
+
7
+ Unfortunately, there's no [single configuration file](https://github.com/dokku/dokku/issues/1558) for dokku.
8
+
9
+ In addition, dokku is really designed for managing a single server. But I'm actually using it to manage multiple servers that are hidden behind a load-balancer.
10
+
11
+ So to manage this, I wanted a single configuration file that I could user for all my dokku information, that could then use that configuration across multiple servers.
12
+
13
+ Currently it's extremely tailored to my needs - it's built for Ubuntu 22.04, it creates a user called "app" (although you can change that), it names your dokku app "app".
14
+
15
+ I've also added in cloudinit configs for some of the other servers I have to use. Of course, these are not related to dokku, but anvil can generate them easily so it's useful to keep them all in one place.
16
+
17
+ There are several [limitations](/docs/roadmap.md) to how it works - it does what I need but does need expansion. That will come soon.
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anvil
4
+ class App
5
+ require_relative "host_deployer"
6
+ class Deploy < Struct.new(:configuration)
7
+ include ConfigurationReader
8
+ def call
9
+ branch = `git rev-parse --abbrev-ref HEAD`.strip
10
+ hosts.each do |host|
11
+ HostDeployer.new(configuration, host.to_s.strip, branch).call
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "standard_procedure/async"
4
+ module Anvil
5
+ require_relative "../logger"
6
+ require_relative "../ssh_executor"
7
+ require_relative "../configuration_reader"
8
+ class App
9
+ class HostDeployer < Struct.new(:configuration, :host, :branch)
10
+ include StandardProcedure::Async::Actor
11
+ include Anvil::ConfigurationReader
12
+
13
+ def call
14
+ first_deployment = !git_remote_exists?
15
+ if first_deployment
16
+ switch_off_downtime_checks
17
+ create_git_remote
18
+ end
19
+
20
+ do_git_push
21
+ if first_deployment
22
+ switch_on_downtime_checks
23
+ run_after_first_deployment_scripts
24
+ scale_processes
25
+ else
26
+ run_after_deployment_scripts
27
+ end
28
+ logger.info "Deployment for #{host} complete"
29
+ end
30
+
31
+ protected
32
+
33
+ def git_remote_exists?
34
+ `git remote show`.split("\n").include? host
35
+ end
36
+
37
+ def switch_off_downtime_checks
38
+ Anvil::SshExecutor.new(host, user_for(host), logger).call do |ssh|
39
+ ssh.exec! "dokku checks:disable app worker,web"
40
+ end
41
+ end
42
+
43
+ def create_git_remote
44
+ logger.info "git remote add #{host} dokku@#{host}:/app"
45
+ logger.info `git remote add #{host} dokku@#{host}:/app`
46
+ end
47
+
48
+ def do_git_push
49
+ logger.info "git push #{host} #{branch}:main"
50
+ logger.info `git push #{host} #{branch}:main`
51
+ end
52
+
53
+ def switch_on_downtime_checks
54
+ Anvil::SshExecutor.new(host, user_for(host), logger).call do |ssh|
55
+ ssh.exec! "dokku checks:enable app web"
56
+ end
57
+ end
58
+
59
+ def run_after_first_deployment_scripts
60
+ Anvil::SshExecutor.new(host, user_for(host), logger).call do |ssh|
61
+ (configuration_for(host).dig("scripts")&.dig("after_first_deploy") || []).each do |script|
62
+ ssh.exec! script, "run_after_install_scripts"
63
+ end
64
+ (configuration_for_app.dig("scripts")&.dig("after_first_deploy") || []).each do |script|
65
+ ssh.exec! script, "run_after_install_scripts"
66
+ end
67
+ end
68
+ end
69
+
70
+ def scale_processes
71
+ Anvil::App::HostScaler.new(configuration, host).call
72
+ end
73
+
74
+ def run_after_deployment_scripts
75
+ Anvil::SshExecutor.new(host, user_for(host), logger).call do |ssh|
76
+ (configuration_for(host).dig("scripts")&.dig("after_deploy") || []).each do |script|
77
+ ssh.exec! script, "run_after_install_scripts"
78
+ end
79
+ (configuration_for_app.dig("scripts")&.dig("after_deploy") || []).each do |script|
80
+ ssh.exec! script, "run_after_install_scripts"
81
+ end
82
+ end
83
+ end
84
+
85
+ def logger
86
+ @logger ||= Anvil::Logger.new(host)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -13,15 +13,22 @@ module Anvil
13
13
 
14
14
  def call
15
15
  Anvil::SshExecutor.new(host, user_for(host), logger).call do |ssh|
16
+ install_plugins ssh
16
17
  create_app ssh
17
18
  set_environment ssh
18
19
  set_dokku_options ssh
19
- run_post_installation_scripts ssh
20
+ run_after_install_scripts ssh
20
21
  end
21
22
  end
22
23
 
23
24
  protected
24
25
 
26
+ def install_plugins ssh
27
+ (configuration_for_app.fetch("plugins") | []).each do |plugin|
28
+ ssh.exec! "sudo dokku plugin:install https://github.com/dokku/dokku-#{plugin}.git #{plugin}"
29
+ end
30
+ end
31
+
25
32
  def create_app ssh
26
33
  ssh.exec! "dokku apps:create app", "create_app"
27
34
  end
@@ -34,27 +41,28 @@ module Anvil
34
41
  ssh.exec! "dokku docker-options:add app run \"--add-host=host.docker.internal:host-gateway\"", "set_dokku_options"
35
42
  ssh.exec! "dokku domains:set app #{configuration_for_app["domain"]}", "set_dokku_options"
36
43
  ssh.exec! "dokku proxy:ports-add app http:80:#{configuration_for_app["port"]}", "set_dokku_options"
44
+ ssh.exec! "dokku proxy:ports-add app https:443:#{configuration_for_app["port"]}", "set_dokku_options"
37
45
  ssh.exec! "dokku nginx:set app client-max-body-size #{configuration_for_app["nginx"]["client_max_body_size"]}", "set_dokku_options"
38
46
  ssh.exec! "dokku nginx:set app proxy-read-timeout #{configuration_for_app["nginx"]["proxy_read_timeout"]}", "set_dokku_options"
39
- if configuration_for_app["nginx"]["forward_proxy_headers"]
40
- ssh.exec! "dokku nginx:set $APP x-forwarded-for-value \"$http_x_forwarded_for\"", "set_dokku_options"
41
- ssh.exec! "dokku nginx:set $APP x-forwarded-port-value \"$http_x_forwarded_port\"", "set_dokku_options"
42
- ssh.exec! "dokku nginx:set $APP x-forwarded-proto-value \"$http_x_forwarded_proto\"", "set_dokku_options"
47
+ if configuration_for_app["load_balancer"]
48
+ ssh.exec! "dokku nginx:set app x-forwarded-for-value \"$http_x_forwarded_for\"", "set_dokku_options"
49
+ ssh.exec! "dokku nginx:set app x-forwarded-port-value \"$http_x_forwarded_port\"", "set_dokku_options"
50
+ ssh.exec! "dokku nginx:set app x-forwarded-proto-value \"$http_x_forwarded_proto\"", "set_dokku_options"
43
51
  end
44
52
  ssh.exec! "dokku proxy:build-config app", "set_dokku_options"
45
53
  end
46
54
 
47
- def run_post_installation_scripts ssh
48
- configuration_for_app.fetch("scripts")&.fetch("post_install")&.each do |script|
49
- ssh.exec! script, "run_post_installation_scripts"
55
+ def run_after_install_scripts ssh
56
+ (configuration_for(host).dig("scripts")&.dig("after_install") || []).each do |script|
57
+ ssh.exec! script, "run_after_install_scripts"
50
58
  end
51
- configuration_for(host).fetch("scripts")&.fetch("post_install")&.each do |script|
52
- ssh.exec! script, "run_post_installation_scripts"
59
+ (configuration_for_app.dig("scripts")&.dig("after_install") || []).each do |script|
60
+ ssh.exec! script, "run_after_install_scripts"
53
61
  end
54
62
  end
55
63
 
56
64
  def logger
57
- @logger ||= Anvil::Logger.new("HostInstaller - #{host}")
65
+ @logger ||= Anvil::Logger.new(host)
58
66
  end
59
67
  end
60
68
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "standard_procedure/async"
4
+ module Anvil
5
+ require_relative "../logger"
6
+ require_relative "../ssh_executor"
7
+ require_relative "../configuration_reader"
8
+ class App
9
+ class HostScaler < Struct.new(:configuration, :host)
10
+ include StandardProcedure::Async::Actor
11
+ include Anvil::ConfigurationReader
12
+
13
+ def call
14
+ scale_processes
15
+ end
16
+
17
+ protected
18
+
19
+ def scale_processes
20
+ scale = configuration_for(host).dig("scale") || configuration_for_app.dig("scale") || "web=1"
21
+ Anvil::SshExecutor.new(host, user_for(host), logger).call do |ssh|
22
+ ssh.exec! "dokku ps:scale app #{scale}"
23
+ end
24
+ end
25
+
26
+ def logger
27
+ @logger ||= Anvil::Logger.new(host)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anvil
4
+ class App
5
+ require_relative "host_scaler"
6
+ class Scale < Struct.new(:configuration)
7
+ include ConfigurationReader
8
+ def call
9
+ hosts.each do |host|
10
+ HostScaler.new(configuration, host.to_s.strip).call
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
data/lib/anvil/app.rb CHANGED
@@ -7,6 +7,8 @@ module Anvil
7
7
  class App < Anvil::SubCommandBase
8
8
  require_relative "app/env"
9
9
  require_relative "app/install"
10
+ require_relative "app/deploy"
11
+ require_relative "app/scale"
10
12
 
11
13
  desc "env /path/to/config.yml", "Generate environment variables for an app"
12
14
  long_desc <<-DESC
@@ -49,6 +51,34 @@ module Anvil
49
51
  Anvil::App::Install.new(configuration, secrets).call
50
52
  end
51
53
 
54
+ desc "deploy /path/to/config.yml", "Deploy an app"
55
+ long_desc <<-DESC
56
+ Deploy an app on the hosts specified in the configuration.
57
+
58
+ First it checks to see if a git remote exists for each host.
59
+
60
+ If not, this counts as a first deployment and it creates the git remote
61
+
62
+ Then, whether first deployment or not, it does a `git push` of the current branch to main on the remote.
63
+
64
+ Finally, if this is the first deployment, it runs the "after_first_deployment" scripts, otherwise it runs the "after_deployment" scripts.
65
+ DESC
66
+ def deploy filename = "deploy.yml"
67
+ configuration = YAML.load_file(filename)
68
+ Anvil::App::Deploy.new(configuration).call
69
+ end
70
+
71
+ desc "scale /path/to/config.yml", "Scale an app"
72
+ long_desc <<-DESC
73
+ Scale a previously deployed app, using the scale values from the config file.
74
+
75
+ The scale can either be set per host or per app (with host settings taking priority).
76
+ DESC
77
+ def scale filename = "deploy.yml"
78
+ configuration = YAML.load_file(filename)
79
+ Anvil::App::Scale.new(configuration).call
80
+ end
81
+
52
82
  protected
53
83
 
54
84
  def read_secrets(filename: nil, stdin: false)
data/lib/anvil/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Anvil
4
- VERSION = "0.1.6"
4
+ VERSION = "0.2.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard-procedure-anvil
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rahoul Baruah
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-07-03 00:00:00.000000000 Z
11
+ date: 2023-08-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -110,22 +110,35 @@ files:
110
110
  - LICENSE.txt
111
111
  - README.md
112
112
  - Rakefile
113
- - assets/cloudinit/dokku.mysql.ubuntu-22.yml
114
- - assets/cloudinit/dokku.ubuntu-22.yml
115
- - assets/cloudinit/memcached.ubuntu-22.yml
116
- - assets/cloudinit/mysql.ubuntu-22.yml
117
- - assets/cloudinit/opensearch.ubuntu-22.yml
118
- - assets/cloudinit/redis.ubuntu-22.yml
119
- - assets/install/dokku.txt
113
+ - assets/cloudinit/docker.yml
114
+ - assets/cloudinit/dokku.yml
115
+ - assets/cloudinit/memcached.yml
116
+ - assets/cloudinit/mysql.yml
117
+ - assets/cloudinit/opensearch.yml
118
+ - assets/cloudinit/redis.yml
120
119
  - checksums/standard-procedure-anvil-0.1.4.gem.sha512
121
120
  - checksums/standard-procedure-anvil-0.1.5.gem.sha512
122
121
  - checksums/standard-procedure-anvil-0.1.6.gem.sha512
122
+ - checksums/standard-procedure-anvil-0.1.7.gem.sha512
123
+ - checksums/standard-procedure-anvil-0.2.0.gem.sha512
124
+ - checksums/standard-procedure-anvil-0.2.1.gem.sha512
125
+ - docs/app.md
126
+ - docs/cloudinit.md
127
+ - docs/configuration.md
128
+ - docs/roadmap.md
129
+ - docs/ruby-on-rails.md
130
+ - docs/secrets.md
131
+ - docs/why.md
123
132
  - exe/anvil
124
133
  - lib/anvil.rb
125
134
  - lib/anvil/app.rb
135
+ - lib/anvil/app/deploy.rb
126
136
  - lib/anvil/app/env.rb
137
+ - lib/anvil/app/host_deployer.rb
127
138
  - lib/anvil/app/host_installer.rb
139
+ - lib/anvil/app/host_scaler.rb
128
140
  - lib/anvil/app/install.rb
141
+ - lib/anvil/app/scale.rb
129
142
  - lib/anvil/cli.rb
130
143
  - lib/anvil/cloudinit.rb
131
144
  - lib/anvil/cloudinit/generator.rb
@@ -1,13 +0,0 @@
1
- # SSH into your server and paste the script
2
- sudo bash
3
- echo "dokku dokku/vhost_enable boolean true" | sudo debconf-set-selections
4
- wget https://dokku.com/install/v0.30.7/bootstrap.sh && sudo DOKKU_TAG=v0.30.7 bash bootstrap.sh
5
- cat /home/app/.ssh/authorized_keys | dokku ssh-keys:add admin
6
- dokku plugin:install https://github.com/dokku/dokku-cron-restart.git cron-restart
7
- dokku plugin:install https://github.com/dokku/dokku-maintenance.git maintenance
8
- dokku plugin:install https://github.com/dokku/dokku-redis.git redis
9
- dokku plugin:install https://github.com/dokku/dokku-memcached.git memcached
10
- dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git letsencrypt
11
- dokku config:set --global DOKKU_LETSENCRYPT_EMAIL=sysadmin@echodek.co
12
- dokku git:set --global deploy-branch main
13
- exit