Skip to content

Commit 993751d

Browse files
baijumclaude
andcommitted
docs: add step-by-step deployment tutorial
Walk users through the full fork-to-running-app process: server provisioning, Docker bootstrap, DNS, GitHub secrets, deploy, and troubleshooting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b1264a0 commit 993751d

2 files changed

Lines changed: 328 additions & 0 deletions

File tree

docs/tutorial.md

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
# Deployment Tutorial
2+
3+
This tutorial walks you through deploying a Towlion application from fork to running service. By the end, you will have a live application on your own server with automatic TLS, a database, and continuous deployment from GitHub.
4+
5+
!!! tip "Before you start"
6+
This is a hands-on guide with concrete commands. For background on *why* the platform works this way, see [Self-Hosting](self-hosting.md) for the fork model and [Deployment](deployment.md) for pipeline internals.
7+
8+
## Prerequisites
9+
10+
You will need:
11+
12+
- A **GitHub account**
13+
- A **Debian 12 server** (VPS from any provider — Hetzner, DigitalOcean, Linode, etc.)
14+
- A **domain name** you control (for DNS configuration)
15+
- A local machine with **Git** and **SSH** installed
16+
17+
| Resource | Minimum |
18+
|---|---|
19+
| CPU | 2 cores |
20+
| RAM | 4 GB |
21+
| Disk | 50 GB |
22+
23+
## Step 1: Fork the app repository
24+
25+
Go to the application repository on GitHub (for example, [towlion/app-template](https://github.com/towlion/app-template)) and click **Fork**.
26+
27+
Then clone your fork locally:
28+
29+
```bash
30+
git clone git@github.com:YOUR_USERNAME/app-template.git
31+
cd app-template
32+
```
33+
34+
!!! tip
35+
If you are creating a new app rather than deploying an existing one, use the **Use this template** button on [towlion/app-template](https://github.com/towlion/app-template) instead of forking. This gives you a clean commit history.
36+
37+
## Step 2: Provision a server
38+
39+
Create a Debian 12 server from your preferred provider. Make sure:
40+
41+
- Ports **22**, **80**, and **443** are open in the firewall
42+
- You can SSH in as a non-root user with sudo access
43+
44+
Verify access:
45+
46+
```bash
47+
ssh deploy@YOUR_SERVER_IP
48+
```
49+
50+
You should see a shell prompt. If this works, you are ready to bootstrap.
51+
52+
## Step 3: Bootstrap the server
53+
54+
SSH into your server and install Docker:
55+
56+
```bash
57+
ssh deploy@YOUR_SERVER_IP
58+
```
59+
60+
Install Docker using the official convenience script:
61+
62+
```bash
63+
curl -fsSL https://get.docker.com | sudo sh
64+
sudo usermod -aG docker $USER
65+
```
66+
67+
!!! warning
68+
Log out and back in after adding yourself to the `docker` group, or the next commands will fail with a permission error.
69+
70+
```bash
71+
exit
72+
ssh deploy@YOUR_SERVER_IP
73+
```
74+
75+
Verify Docker is working:
76+
77+
```bash
78+
docker run --rm hello-world
79+
```
80+
81+
You should see `Hello from Docker!` in the output.
82+
83+
Create the data directory structure:
84+
85+
```bash
86+
sudo mkdir -p /data/{postgres,redis,minio,caddy}
87+
sudo chown -R $USER:$USER /data
88+
```
89+
90+
This is where persistent data lives across deployments. The directory layout:
91+
92+
```
93+
/data/
94+
postgres/ # Database files
95+
redis/ # Cache and queue data
96+
minio/ # Object storage
97+
caddy/ # TLS certificates and config
98+
```
99+
100+
## Step 4: Configure DNS
101+
102+
Go to your domain registrar or DNS provider and add an **A record** pointing to your server:
103+
104+
```
105+
Type: A
106+
Name: app (or your chosen subdomain)
107+
Value: YOUR_SERVER_IP
108+
TTL: 300
109+
```
110+
111+
For example, if your domain is `example.com` and your server IP is `203.0.113.42`:
112+
113+
```
114+
A record: app.example.com -> 203.0.113.42
115+
```
116+
117+
Verify DNS propagation:
118+
119+
```bash
120+
dig +short app.example.com
121+
```
122+
123+
Expected output:
124+
125+
```
126+
203.0.113.42
127+
```
128+
129+
!!! tip
130+
DNS propagation can take a few minutes to a few hours. Wait until `dig` returns your server IP before proceeding.
131+
132+
## Step 5: Configure GitHub secrets
133+
134+
In your forked repository on GitHub, go to **Settings > Secrets and variables > Actions** and add the following repository secrets:
135+
136+
| Secret | Example value | Description |
137+
|---|---|---|
138+
| `SERVER_HOST` | `203.0.113.42` | Your server's IP address |
139+
| `SERVER_USER` | `deploy` | SSH username on the server |
140+
| `SERVER_SSH_KEY` | *(private key contents)* | SSH private key for deployment |
141+
| `APP_DOMAIN` | `app.example.com` | Domain pointing to your server |
142+
| `DATABASE_PASSWORD` | *(strong password)* | PostgreSQL password |
143+
| `MINIO_ROOT_USER` | `minio-admin` | MinIO admin username |
144+
| `MINIO_ROOT_PASSWORD` | *(strong password)* | MinIO admin password |
145+
146+
### Generate a deploy SSH key
147+
148+
Create a dedicated key pair for deployment:
149+
150+
```bash
151+
ssh-keygen -t ed25519 -f ~/.ssh/deploy_key -N ""
152+
```
153+
154+
Add the **public** key to your server:
155+
156+
```bash
157+
ssh-copy-id -i ~/.ssh/deploy_key.pub deploy@YOUR_SERVER_IP
158+
```
159+
160+
Copy the **private** key contents into the `SERVER_SSH_KEY` secret:
161+
162+
```bash
163+
cat ~/.ssh/deploy_key
164+
```
165+
166+
Paste the full output (including the `-----BEGIN` and `-----END` lines) into the secret value field on GitHub.
167+
168+
## Step 6: Deploy
169+
170+
Push a commit to the `main` branch to trigger deployment:
171+
172+
```bash
173+
git push origin main
174+
```
175+
176+
GitHub Actions picks this up automatically. Go to the **Actions** tab in your repository to watch the workflow run.
177+
178+
```
179+
Push to main
180+
|
181+
v
182+
GitHub Actions
183+
|
184+
+-- Run tests
185+
+-- SSH into server
186+
+-- Pull latest code
187+
+-- Build containers
188+
+-- Run database migrations
189+
+-- Start services
190+
+-- Health check
191+
```
192+
193+
The workflow typically completes in 2-5 minutes.
194+
195+
!!! tip
196+
If the workflow does not appear, check that the `.github/workflows/deploy.yml` file exists in your repository. Repositories created from the app template include this file by default.
197+
198+
## Step 7: Verify
199+
200+
Once the workflow succeeds, check your application is running.
201+
202+
Test the health endpoint:
203+
204+
```bash
205+
curl https://app.example.com/health
206+
```
207+
208+
Expected response:
209+
210+
```json
211+
{"status": "ok"}
212+
```
213+
214+
Open `https://app.example.com` in your browser. You should see your application with a valid TLS certificate (Caddy provisions this automatically via Let's Encrypt).
215+
216+
Your application is now live.
217+
218+
## Updating your app
219+
220+
To deploy changes, commit and push to `main`:
221+
222+
```bash
223+
git add .
224+
git commit -m "feat: add new feature"
225+
git push origin main
226+
```
227+
228+
GitHub Actions runs the deployment pipeline automatically. The platform uses [rolling updates](deployment.md#zero-downtime-deployments) so your application stays available during deploys.
229+
230+
To pull upstream changes from the original repository:
231+
232+
```bash
233+
git remote add upstream https://github.com/towlion/app-template.git
234+
git fetch upstream
235+
git merge upstream/main
236+
git push origin main
237+
```
238+
239+
## Troubleshooting
240+
241+
### DNS not resolving
242+
243+
**Symptom**: `dig +short app.example.com` returns nothing.
244+
245+
**Fix**: Wait for DNS propagation (up to 48 hours in rare cases). Verify the A record is set correctly in your DNS provider's dashboard. Try flushing your local DNS cache:
246+
247+
```bash
248+
# macOS
249+
sudo dscacheutil -flushcache
250+
251+
# Linux
252+
sudo systemd-resolve --flush-caches
253+
```
254+
255+
### SSH key rejected
256+
257+
**Symptom**: GitHub Actions workflow fails with `Permission denied (publickey)`.
258+
259+
**Fix**: Verify the `SERVER_SSH_KEY` secret contains the full private key including header and footer lines. Ensure the corresponding public key is in `~/.ssh/authorized_keys` on the server. Check that the key format is correct:
260+
261+
```bash
262+
# The secret should start with:
263+
-----BEGIN OPENSSH PRIVATE KEY-----
264+
265+
# And end with:
266+
-----END OPENSSH PRIVATE KEY-----
267+
```
268+
269+
### Health check fails
270+
271+
**Symptom**: Deployment completes but `curl https://app.example.com/health` returns an error.
272+
273+
**Fix**: SSH into the server and check container status:
274+
275+
```bash
276+
ssh deploy@YOUR_SERVER_IP
277+
docker compose ps
278+
```
279+
280+
All services should show `Up` status. Check application logs:
281+
282+
```bash
283+
docker compose logs app --tail 50
284+
```
285+
286+
Common causes:
287+
288+
- Database migration failed — check `docker compose logs app` for migration errors
289+
- Missing environment variable — verify all secrets are set in GitHub
290+
- Port conflict — ensure no other service is using ports 80 or 443
291+
292+
### Containers not starting
293+
294+
**Symptom**: `docker compose ps` shows containers in `Restarting` or `Exit` state.
295+
296+
**Fix**: Check the logs for the failing container:
297+
298+
```bash
299+
docker compose logs postgres --tail 50
300+
docker compose logs app --tail 50
301+
```
302+
303+
If PostgreSQL fails to start, verify the `/data/postgres` directory exists and has correct permissions:
304+
305+
```bash
306+
ls -la /data/postgres
307+
```
308+
309+
### TLS certificate not provisioning
310+
311+
**Symptom**: Browser shows a certificate warning when visiting your domain.
312+
313+
**Fix**: Caddy provisions TLS certificates automatically, but requires:
314+
315+
1. DNS is correctly pointing to your server
316+
2. Ports 80 and 443 are open and reachable from the internet
317+
3. The domain is set correctly in your app configuration
318+
319+
Check Caddy logs:
320+
321+
```bash
322+
docker compose logs caddy --tail 50
323+
```
324+
325+
---
326+
327+
For more details on the deployment pipeline, see [Deployment](deployment.md). For the full list of application requirements, see the [App Specification](spec.md).

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ theme:
3030

3131
nav:
3232
- Home: index.md
33+
- Tutorial: tutorial.md
3334
- Architecture: architecture.md
3435
- App Specification: spec.md
3536
- Deployment: deployment.md

0 commit comments

Comments
 (0)