init
This commit is contained in:
commit
52843aa9e7
13 changed files with 920 additions and 0 deletions
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Terraform
|
||||||
|
*.tfstate
|
||||||
|
*.tfstate.*
|
||||||
|
.terraform/
|
||||||
|
.terraform.lock.hcl
|
||||||
|
crash.log
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
terraform.tfvars
|
||||||
|
files/env/*.env
|
||||||
|
!files/env/*.example
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
.vscode/
|
||||||
11
.woodpecker.yml
Normal file
11
.woodpecker.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
when:
|
||||||
|
branch: main
|
||||||
|
event: push
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: validate
|
||||||
|
image: hashicorp/terraform:1.10
|
||||||
|
commands:
|
||||||
|
- terraform init -backend=false
|
||||||
|
- terraform validate
|
||||||
|
- terraform fmt -check
|
||||||
109
files/cloud-init/ops.yml
Normal file
109
files/cloud-init/ops.yml
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
#cloud-config
|
||||||
|
|
||||||
|
package_update: true
|
||||||
|
package_upgrade: true
|
||||||
|
|
||||||
|
packages:
|
||||||
|
- docker.io
|
||||||
|
- docker-compose-v2
|
||||||
|
- git
|
||||||
|
- curl
|
||||||
|
- sqlite3
|
||||||
|
|
||||||
|
users:
|
||||||
|
- name: deploy
|
||||||
|
groups: docker, sudo
|
||||||
|
shell: /bin/bash
|
||||||
|
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||||
|
ssh_authorized_keys:
|
||||||
|
- ${ssh_public_key}
|
||||||
|
|
||||||
|
write_files:
|
||||||
|
- path: /opt/writekit/.env
|
||||||
|
permissions: '0600'
|
||||||
|
content: |
|
||||||
|
${indent(6, env_file)}
|
||||||
|
|
||||||
|
- path: /opt/writekit/docker-compose.yml
|
||||||
|
permissions: '0644'
|
||||||
|
content: |
|
||||||
|
${indent(6, docker_compose)}
|
||||||
|
|
||||||
|
- path: /etc/docker/daemon.json
|
||||||
|
permissions: '0644'
|
||||||
|
content: |
|
||||||
|
{
|
||||||
|
"insecure-registries": ["10.0.0.3:5000"],
|
||||||
|
"log-driver": "json-file",
|
||||||
|
"log-opts": {
|
||||||
|
"max-size": "10m",
|
||||||
|
"max-file": "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- path: /opt/writekit/.ssh/deploy_key
|
||||||
|
permissions: '0600'
|
||||||
|
content: |
|
||||||
|
${indent(6, deploy_ssh_private_key)}
|
||||||
|
|
||||||
|
- path: /opt/writekit/setup-forgejo-oauth.sh
|
||||||
|
permissions: '0755'
|
||||||
|
content: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -a
|
||||||
|
. /opt/writekit/.env
|
||||||
|
set +a
|
||||||
|
|
||||||
|
DB="/var/lib/docker/volumes/writekit_forgejo-data/_data/gitea/gitea.db"
|
||||||
|
|
||||||
|
for i in {1..60}; do
|
||||||
|
[ -f "$$DB" ] && break
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
sqlite3 "$$DB" <<EOF
|
||||||
|
INSERT OR IGNORE INTO login_source (type, name, is_active, is_sync_enabled, cfg, created_unix, updated_unix)
|
||||||
|
VALUES (6, 'GitHub', 1, 0, '{"Provider":"github","ClientID":"$$GITHUB_CLIENT_ID","ClientSecret":"$$GITHUB_CLIENT_SECRET","OpenIDConnectAutoDiscoveryURL":"","CustomURLMapping":null,"IconURL":"","Scopes":["read:user","user:email"],"RequiredClaimName":"","RequiredClaimValue":"","GroupClaimName":"","AdminGroup":"","RestrictedGroup":"","GroupTeamMap":"","GroupTeamMapRemoval":false}', strftime('%s','now'), strftime('%s','now'));
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO oauth2_application (uid, name, client_id, client_secret, confidential_client, redirect_uris, created_unix, updated_unix)
|
||||||
|
VALUES (0, 'Woodpecker CI', '$$WOODPECKER_FORGEJO_CLIENT', '$$WOODPECKER_FORGEJO_SECRET', 1, 'https://ci.$$DOMAIN/authorize', strftime('%s','now'), strftime('%s','now'));
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- path: /opt/writekit/promote-admin.sh
|
||||||
|
permissions: '0755'
|
||||||
|
content: |
|
||||||
|
#!/bin/bash
|
||||||
|
set -a
|
||||||
|
. /opt/writekit/.env
|
||||||
|
set +a
|
||||||
|
cd /opt/writekit
|
||||||
|
docker compose exec -T forgejo gitea admin user change-password --username "$$WOODPECKER_ADMIN" --password "temppass123" 2>/dev/null || true
|
||||||
|
docker compose exec -T forgejo gitea admin user create --username "$$WOODPECKER_ADMIN" --email "$${WOODPECKER_ADMIN}@localhost" --password "temppass123" --admin 2>/dev/null || \
|
||||||
|
docker compose exec -T forgejo gitea admin user change-password --username "$$WOODPECKER_ADMIN" --must-change-password=false 2>/dev/null
|
||||||
|
sqlite3 "/var/lib/docker/volumes/writekit_forgejo-data/_data/gitea/gitea.db" "UPDATE user SET is_admin=1 WHERE lower_name='$$(echo $$WOODPECKER_ADMIN | tr '[:upper:]' '[:lower:]')';"
|
||||||
|
echo "User $$WOODPECKER_ADMIN promoted to admin"
|
||||||
|
|
||||||
|
runcmd:
|
||||||
|
- systemctl enable docker
|
||||||
|
- systemctl start docker
|
||||||
|
- mkdir -p /opt/writekit/.ssh
|
||||||
|
- chown -R deploy:deploy /opt/writekit
|
||||||
|
|
||||||
|
- |
|
||||||
|
set -a
|
||||||
|
. /opt/writekit/.env
|
||||||
|
set +a
|
||||||
|
cd /opt/writekit && docker compose up -d
|
||||||
|
|
||||||
|
- /opt/writekit/setup-forgejo-oauth.sh
|
||||||
|
|
||||||
|
- |
|
||||||
|
for i in {1..30}; do
|
||||||
|
ssh-keyscan -H 10.0.0.2 >> /opt/writekit/.ssh/known_hosts 2>/dev/null && break
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
chown deploy:deploy /opt/writekit/.ssh/known_hosts
|
||||||
|
|
||||||
|
final_message: "WriteKit ops server ready after $$UPTIME seconds"
|
||||||
58
files/cloud-init/prod.yml
Normal file
58
files/cloud-init/prod.yml
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
#cloud-config
|
||||||
|
|
||||||
|
package_update: true
|
||||||
|
package_upgrade: true
|
||||||
|
|
||||||
|
packages:
|
||||||
|
- docker.io
|
||||||
|
- docker-compose-v2
|
||||||
|
- git
|
||||||
|
- curl
|
||||||
|
|
||||||
|
users:
|
||||||
|
- name: deploy
|
||||||
|
groups: docker, sudo
|
||||||
|
shell: /bin/bash
|
||||||
|
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||||
|
ssh_authorized_keys:
|
||||||
|
- ${ssh_public_key}
|
||||||
|
- ${deploy_ssh_public_key}
|
||||||
|
|
||||||
|
write_files:
|
||||||
|
- path: /opt/writekit/.env
|
||||||
|
permissions: '0600'
|
||||||
|
content: |
|
||||||
|
${indent(6, env_file)}
|
||||||
|
|
||||||
|
- path: /opt/writekit/docker-compose.yml
|
||||||
|
permissions: '0644'
|
||||||
|
content: |
|
||||||
|
${indent(6, docker_compose)}
|
||||||
|
|
||||||
|
- path: /etc/docker/daemon.json
|
||||||
|
permissions: '0644'
|
||||||
|
content: |
|
||||||
|
{
|
||||||
|
"insecure-registries": ["10.0.0.3:5000"],
|
||||||
|
"log-driver": "json-file",
|
||||||
|
"log-opts": {
|
||||||
|
"max-size": "10m",
|
||||||
|
"max-file": "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runcmd:
|
||||||
|
- systemctl enable docker
|
||||||
|
- systemctl start docker
|
||||||
|
- mkdir -p /opt/writekit/data/tenants
|
||||||
|
- chown -R deploy:deploy /opt/writekit
|
||||||
|
|
||||||
|
- |
|
||||||
|
set -a
|
||||||
|
. /opt/writekit/.env
|
||||||
|
set +a
|
||||||
|
cd /opt/writekit && docker compose up -d postgres traefik
|
||||||
|
sleep 10
|
||||||
|
docker compose exec -T postgres psql -U writekit -c "CREATE DATABASE writekit_staging;" || true
|
||||||
|
|
||||||
|
final_message: "WriteKit prod server ready after $$UPTIME seconds"
|
||||||
12
files/env/ops.env.example
vendored
Normal file
12
files/env/ops.env.example
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
DOMAIN=writekit.dev
|
||||||
|
ACME_EMAIL=you@example.com
|
||||||
|
|
||||||
|
CF_DNS_API_TOKEN=
|
||||||
|
|
||||||
|
GITHUB_CLIENT_ID=
|
||||||
|
GITHUB_CLIENT_SECRET=
|
||||||
|
|
||||||
|
WOODPECKER_AGENT_SECRET=
|
||||||
|
WOODPECKER_FORGEJO_CLIENT=
|
||||||
|
WOODPECKER_FORGEJO_SECRET=
|
||||||
|
WOODPECKER_ADMIN=josh
|
||||||
41
files/env/prod.env.example
vendored
Normal file
41
files/env/prod.env.example
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
DOMAIN=writekit.dev
|
||||||
|
REGISTRY_URL=10.0.0.3:5000
|
||||||
|
ACME_EMAIL=you@example.com
|
||||||
|
|
||||||
|
PG_PASSWORD=
|
||||||
|
|
||||||
|
CF_DNS_API_TOKEN=
|
||||||
|
|
||||||
|
R2_ACCOUNT_ID=
|
||||||
|
R2_ACCESS_KEY_ID=
|
||||||
|
R2_SECRET_ACCESS_KEY=
|
||||||
|
R2_BUCKET=writekit-assets
|
||||||
|
R2_PUBLIC_URL=https://assets.writekit.dev
|
||||||
|
|
||||||
|
GITHUB_CLIENT_ID=
|
||||||
|
GITHUB_CLIENT_SECRET=
|
||||||
|
|
||||||
|
GOOGLE_CLIENT_ID=
|
||||||
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
DISCORD_CLIENT_ID=
|
||||||
|
DISCORD_CLIENT_SECRET=
|
||||||
|
|
||||||
|
SESSION_SECRET=
|
||||||
|
|
||||||
|
LEMON_API_KEY=
|
||||||
|
LEMON_STORE_ID=
|
||||||
|
LEMON_WEBHOOK_SECRET=
|
||||||
|
|
||||||
|
WISE_API_KEY=
|
||||||
|
WISE_PROFILE_ID=
|
||||||
|
|
||||||
|
OAUTH2_PROXY_COOKIE_SECRET=
|
||||||
|
OAUTH2_PROXY_ALLOWED_USERS=josh
|
||||||
|
|
||||||
|
GITHUB_CLIENT_ID_STAGING=
|
||||||
|
GITHUB_CLIENT_SECRET_STAGING=
|
||||||
|
SESSION_SECRET_STAGING=
|
||||||
|
LEMON_API_KEY_STAGING=
|
||||||
|
LEMON_WEBHOOK_SECRET_STAGING=
|
||||||
|
WISE_API_KEY_STAGING=
|
||||||
|
WISE_PROFILE_ID_STAGING=
|
||||||
118
files/stacks/docker-compose.ops.yml
Normal file
118
files/stacks/docker-compose.ops.yml
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v3.6
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
command:
|
||||||
|
- --api.dashboard=false
|
||||||
|
- --providers.docker=true
|
||||||
|
- --providers.docker.exposedbydefault=false
|
||||||
|
- --providers.docker.network=ops
|
||||||
|
- --entrypoints.web.address=:80
|
||||||
|
- --entrypoints.websecure.address=:443
|
||||||
|
- --entrypoints.web.http.redirections.entrypoint.to=websecure
|
||||||
|
- --entrypoints.web.http.redirections.entrypoint.scheme=https
|
||||||
|
- --certificatesresolvers.cloudflare.acme.dnschallenge=true
|
||||||
|
- --certificatesresolvers.cloudflare.acme.dnschallenge.provider=cloudflare
|
||||||
|
- --certificatesresolvers.cloudflare.acme.email=$${ACME_EMAIL}
|
||||||
|
- --certificatesresolvers.cloudflare.acme.storage=/letsencrypt/acme.json
|
||||||
|
environment:
|
||||||
|
- CF_DNS_API_TOKEN=$${CF_DNS_API_TOKEN}
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- traefik-certs:/letsencrypt
|
||||||
|
networks:
|
||||||
|
- ops
|
||||||
|
|
||||||
|
registry:
|
||||||
|
image: registry:2
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
- registry-data:/var/lib/registry
|
||||||
|
environment:
|
||||||
|
- REGISTRY_STORAGE_DELETE_ENABLED=true
|
||||||
|
networks:
|
||||||
|
- ops
|
||||||
|
|
||||||
|
forgejo:
|
||||||
|
image: codeberg.org/forgejo/forgejo:11
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- USER_UID=1000
|
||||||
|
- USER_GID=1000
|
||||||
|
- FORGEJO__server__DOMAIN=source.$${DOMAIN}
|
||||||
|
- FORGEJO__server__ROOT_URL=https://source.$${DOMAIN}
|
||||||
|
- FORGEJO__server__SSH_DOMAIN=source.$${DOMAIN}
|
||||||
|
- FORGEJO__server__SSH_PORT=22
|
||||||
|
- FORGEJO__server__SSH_LISTEN_PORT=2222
|
||||||
|
- FORGEJO__database__DB_TYPE=sqlite3
|
||||||
|
- FORGEJO__service__DISABLE_REGISTRATION=true
|
||||||
|
- FORGEJO__service__ALLOW_ONLY_EXTERNAL_REGISTRATION=true
|
||||||
|
- FORGEJO__webhook__ALLOWED_HOST_LIST=external,loopback,10.0.0.0/24
|
||||||
|
- FORGEJO__security__INSTALL_LOCK=true
|
||||||
|
- FORGEJO__actions__ENABLED=false
|
||||||
|
volumes:
|
||||||
|
- forgejo-data:/data
|
||||||
|
ports:
|
||||||
|
- "2222:2222"
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.forgejo.rule=Host(`source.$${DOMAIN}`)
|
||||||
|
- traefik.http.routers.forgejo.tls=true
|
||||||
|
- traefik.http.routers.forgejo.tls.certresolver=cloudflare
|
||||||
|
- traefik.http.services.forgejo.loadbalancer.server.port=3000
|
||||||
|
networks:
|
||||||
|
- ops
|
||||||
|
|
||||||
|
woodpecker:
|
||||||
|
image: woodpeckerci/woodpecker-server:v3
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- WOODPECKER_HOST=https://ci.$${DOMAIN}
|
||||||
|
- WOODPECKER_FORGEJO=true
|
||||||
|
- WOODPECKER_FORGEJO_URL=http://forgejo:3000
|
||||||
|
- WOODPECKER_FORGEJO_CLIENT=$${WOODPECKER_FORGEJO_CLIENT}
|
||||||
|
- WOODPECKER_FORGEJO_SECRET=$${WOODPECKER_FORGEJO_SECRET}
|
||||||
|
- WOODPECKER_AGENT_SECRET=$${WOODPECKER_AGENT_SECRET}
|
||||||
|
- WOODPECKER_ADMIN=$${WOODPECKER_ADMIN}
|
||||||
|
volumes:
|
||||||
|
- woodpecker-data:/var/lib/woodpecker
|
||||||
|
depends_on:
|
||||||
|
- forgejo
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.woodpecker.rule=Host(`ci.$${DOMAIN}`)
|
||||||
|
- traefik.http.routers.woodpecker.tls=true
|
||||||
|
- traefik.http.routers.woodpecker.tls.certresolver=cloudflare
|
||||||
|
- traefik.http.services.woodpecker.loadbalancer.server.port=8000
|
||||||
|
networks:
|
||||||
|
- ops
|
||||||
|
|
||||||
|
woodpecker-agent:
|
||||||
|
image: woodpeckerci/woodpecker-agent:v3
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- WOODPECKER_SERVER=woodpecker:9000
|
||||||
|
- WOODPECKER_AGENT_SECRET=$${WOODPECKER_AGENT_SECRET}
|
||||||
|
- WOODPECKER_BACKEND_DOCKER_NETWORK=writekit_ops
|
||||||
|
- WOODPECKER_BACKEND_DOCKER_VOLUMES=/opt/writekit/.ssh:/mnt/ssh:ro
|
||||||
|
- DOCKER_HOST=unix:///var/run/docker.sock
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
depends_on:
|
||||||
|
- woodpecker
|
||||||
|
networks:
|
||||||
|
- ops
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ops:
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
traefik-certs:
|
||||||
|
registry-data:
|
||||||
|
forgejo-data:
|
||||||
|
woodpecker-data:
|
||||||
187
files/stacks/docker-compose.prod.yml
Normal file
187
files/stacks/docker-compose.prod.yml
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v3.6
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
command:
|
||||||
|
- --api.dashboard=false
|
||||||
|
- --providers.docker=true
|
||||||
|
- --providers.docker.exposedbydefault=false
|
||||||
|
- --providers.docker.network=prod
|
||||||
|
- --entrypoints.web.address=:80
|
||||||
|
- --entrypoints.websecure.address=:443
|
||||||
|
- --entrypoints.web.http.redirections.entrypoint.to=websecure
|
||||||
|
- --entrypoints.web.http.redirections.entrypoint.scheme=https
|
||||||
|
- --certificatesresolvers.cloudflare.acme.dnschallenge=true
|
||||||
|
- --certificatesresolvers.cloudflare.acme.dnschallenge.provider=cloudflare
|
||||||
|
- --certificatesresolvers.cloudflare.acme.email=$${ACME_EMAIL}
|
||||||
|
- --certificatesresolvers.cloudflare.acme.storage=/letsencrypt/acme.json
|
||||||
|
environment:
|
||||||
|
- CF_DNS_API_TOKEN=$${CF_DNS_API_TOKEN}
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- traefik-certs:/letsencrypt
|
||||||
|
networks:
|
||||||
|
- prod
|
||||||
|
|
||||||
|
imaginary:
|
||||||
|
image: h2non/imaginary:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
command: -enable-url-source=false -max-allowed-size=15728640
|
||||||
|
networks:
|
||||||
|
- prod
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=writekit
|
||||||
|
- POSTGRES_USER=writekit
|
||||||
|
- POSTGRES_PASSWORD=$${PG_PASSWORD}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U writekit"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- prod
|
||||||
|
|
||||||
|
oauth2-proxy:
|
||||||
|
image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- OAUTH2_PROXY_PROVIDER=github
|
||||||
|
- OAUTH2_PROXY_CLIENT_ID=$${GITHUB_CLIENT_ID_STAGING}
|
||||||
|
- OAUTH2_PROXY_CLIENT_SECRET=$${GITHUB_CLIENT_SECRET_STAGING}
|
||||||
|
- OAUTH2_PROXY_COOKIE_SECRET=$${OAUTH2_PROXY_COOKIE_SECRET}
|
||||||
|
- OAUTH2_PROXY_COOKIE_DOMAINS=.$${DOMAIN}
|
||||||
|
- OAUTH2_PROXY_EMAIL_DOMAINS=*
|
||||||
|
- OAUTH2_PROXY_GITHUB_USERS=$${OAUTH2_PROXY_ALLOWED_USERS}
|
||||||
|
- OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180
|
||||||
|
- OAUTH2_PROXY_REVERSE_PROXY=true
|
||||||
|
- OAUTH2_PROXY_SET_XAUTHREQUEST=true
|
||||||
|
- OAUTH2_PROXY_PASS_ACCESS_TOKEN=true
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.oauth2-proxy.rule=Host(`auth.staging.$${DOMAIN}`)
|
||||||
|
- traefik.http.routers.oauth2-proxy.tls=true
|
||||||
|
- traefik.http.routers.oauth2-proxy.tls.certresolver=cloudflare
|
||||||
|
- traefik.http.services.oauth2-proxy.loadbalancer.server.port=4180
|
||||||
|
- traefik.http.middlewares.staging-auth.forwardauth.address=http://oauth2-proxy:4180/oauth2/auth
|
||||||
|
- traefik.http.middlewares.staging-auth.forwardauth.trustForwardHeader=true
|
||||||
|
- traefik.http.middlewares.staging-auth.forwardauth.authResponseHeaders=X-Auth-Request-User,X-Auth-Request-Email
|
||||||
|
networks:
|
||||||
|
- prod
|
||||||
|
|
||||||
|
writekit-prod:
|
||||||
|
image: $${REGISTRY_URL}/writekit:main
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- ENV=prod
|
||||||
|
- DOMAIN=$${DOMAIN}
|
||||||
|
- BASE_URL=https://$${DOMAIN}
|
||||||
|
- DATABASE_URL=postgres://writekit:$${PG_PASSWORD}@postgres:5432/writekit?sslmode=disable
|
||||||
|
- DATA_DIR=/data
|
||||||
|
- R2_ACCOUNT_ID=$${R2_ACCOUNT_ID}
|
||||||
|
- R2_ACCESS_KEY_ID=$${R2_ACCESS_KEY_ID}
|
||||||
|
- R2_SECRET_ACCESS_KEY=$${R2_SECRET_ACCESS_KEY}
|
||||||
|
- R2_BUCKET=$${R2_BUCKET}
|
||||||
|
- R2_PUBLIC_URL=$${R2_PUBLIC_URL}
|
||||||
|
- GITHUB_CLIENT_ID=$${GITHUB_CLIENT_ID}
|
||||||
|
- GITHUB_CLIENT_SECRET=$${GITHUB_CLIENT_SECRET}
|
||||||
|
- GOOGLE_CLIENT_ID=$${GOOGLE_CLIENT_ID}
|
||||||
|
- GOOGLE_CLIENT_SECRET=$${GOOGLE_CLIENT_SECRET}
|
||||||
|
- DISCORD_CLIENT_ID=$${DISCORD_CLIENT_ID}
|
||||||
|
- DISCORD_CLIENT_SECRET=$${DISCORD_CLIENT_SECRET}
|
||||||
|
- SESSION_SECRET=$${SESSION_SECRET}
|
||||||
|
- LEMON_API_KEY=$${LEMON_API_KEY}
|
||||||
|
- LEMON_STORE_ID=$${LEMON_STORE_ID}
|
||||||
|
- LEMON_WEBHOOK_SECRET=$${LEMON_WEBHOOK_SECRET}
|
||||||
|
- WISE_API_KEY=$${WISE_API_KEY}
|
||||||
|
- WISE_PROFILE_ID=$${WISE_PROFILE_ID}
|
||||||
|
- IMAGINARY_URL=http://imaginary:9000
|
||||||
|
volumes:
|
||||||
|
- tenants-prod:/data
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.writekit-prod-platform.rule=Host(`$${DOMAIN}`)
|
||||||
|
- traefik.http.routers.writekit-prod-platform.tls=true
|
||||||
|
- traefik.http.routers.writekit-prod-platform.tls.certresolver=cloudflare
|
||||||
|
- traefik.http.routers.writekit-prod-platform.service=writekit-prod
|
||||||
|
- traefik.http.routers.writekit-prod-blogs.rule=HostRegexp(`^(?!staging\.).+\.$${DOMAIN}$$`)
|
||||||
|
- traefik.http.routers.writekit-prod-blogs.priority=10
|
||||||
|
- traefik.http.routers.writekit-prod-blogs.tls=true
|
||||||
|
- traefik.http.routers.writekit-prod-blogs.tls.certresolver=cloudflare
|
||||||
|
- traefik.http.routers.writekit-prod-blogs.tls.domains[0].main=$${DOMAIN}
|
||||||
|
- traefik.http.routers.writekit-prod-blogs.tls.domains[0].sans=*.$${DOMAIN}
|
||||||
|
- traefik.http.routers.writekit-prod-blogs.service=writekit-prod
|
||||||
|
- traefik.http.services.writekit-prod.loadbalancer.server.port=8080
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- prod
|
||||||
|
|
||||||
|
writekit-staging:
|
||||||
|
image: $${REGISTRY_URL}/writekit:dev
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- ENV=staging
|
||||||
|
- DOMAIN=staging.$${DOMAIN}
|
||||||
|
- BASE_URL=https://staging.$${DOMAIN}
|
||||||
|
- DATABASE_URL=postgres://writekit:$${PG_PASSWORD}@postgres:5432/writekit_staging?sslmode=disable
|
||||||
|
- DATA_DIR=/data
|
||||||
|
- R2_ACCOUNT_ID=$${R2_ACCOUNT_ID}
|
||||||
|
- R2_ACCESS_KEY_ID=$${R2_ACCESS_KEY_ID}
|
||||||
|
- R2_SECRET_ACCESS_KEY=$${R2_SECRET_ACCESS_KEY}
|
||||||
|
- R2_BUCKET=$${R2_BUCKET}
|
||||||
|
- R2_PUBLIC_URL=$${R2_PUBLIC_URL}
|
||||||
|
- GITHUB_CLIENT_ID=$${GITHUB_CLIENT_ID_STAGING}
|
||||||
|
- GITHUB_CLIENT_SECRET=$${GITHUB_CLIENT_SECRET_STAGING}
|
||||||
|
- GOOGLE_CLIENT_ID=$${GOOGLE_CLIENT_ID}
|
||||||
|
- GOOGLE_CLIENT_SECRET=$${GOOGLE_CLIENT_SECRET}
|
||||||
|
- DISCORD_CLIENT_ID=$${DISCORD_CLIENT_ID}
|
||||||
|
- DISCORD_CLIENT_SECRET=$${DISCORD_CLIENT_SECRET}
|
||||||
|
- SESSION_SECRET=$${SESSION_SECRET_STAGING}
|
||||||
|
- LEMON_API_KEY=$${LEMON_API_KEY_STAGING}
|
||||||
|
- LEMON_STORE_ID=$${LEMON_STORE_ID}
|
||||||
|
- LEMON_WEBHOOK_SECRET=$${LEMON_WEBHOOK_SECRET_STAGING}
|
||||||
|
- WISE_API_KEY=$${WISE_API_KEY_STAGING}
|
||||||
|
- WISE_PROFILE_ID=$${WISE_PROFILE_ID_STAGING}
|
||||||
|
- IMAGINARY_URL=http://imaginary:9000
|
||||||
|
volumes:
|
||||||
|
- tenants-staging:/data
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.writekit-staging-platform.rule=Host(`staging.$${DOMAIN}`)
|
||||||
|
- traefik.http.routers.writekit-staging-platform.tls=true
|
||||||
|
- traefik.http.routers.writekit-staging-platform.tls.certresolver=cloudflare
|
||||||
|
- traefik.http.routers.writekit-staging-platform.middlewares=staging-auth
|
||||||
|
- traefik.http.routers.writekit-staging-platform.service=writekit-staging
|
||||||
|
- traefik.http.routers.writekit-staging-blogs.rule=HostRegexp(`^.+\.staging\.$${DOMAIN}$$`)
|
||||||
|
- traefik.http.routers.writekit-staging-blogs.priority=20
|
||||||
|
- traefik.http.routers.writekit-staging-blogs.tls=true
|
||||||
|
- traefik.http.routers.writekit-staging-blogs.tls.certresolver=cloudflare
|
||||||
|
- traefik.http.routers.writekit-staging-blogs.tls.domains[0].main=staging.$${DOMAIN}
|
||||||
|
- traefik.http.routers.writekit-staging-blogs.tls.domains[0].sans=*.staging.$${DOMAIN}
|
||||||
|
- traefik.http.routers.writekit-staging-blogs.middlewares=staging-auth
|
||||||
|
- traefik.http.routers.writekit-staging-blogs.service=writekit-staging
|
||||||
|
- traefik.http.services.writekit-staging.loadbalancer.server.port=8080
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- prod
|
||||||
|
|
||||||
|
networks:
|
||||||
|
prod:
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
traefik-certs:
|
||||||
|
postgres-data:
|
||||||
|
tenants-prod:
|
||||||
|
tenants-staging:
|
||||||
264
main.tf
Normal file
264
main.tf
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
locals {
|
||||||
|
prod_env_file = file("${path.module}/files/env/prod.env")
|
||||||
|
prod_compose = file("${path.module}/files/stacks/docker-compose.prod.yml")
|
||||||
|
ops_env_file = file("${path.module}/files/env/ops.env")
|
||||||
|
ops_compose = file("${path.module}/files/stacks/docker-compose.ops.yml")
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "tls_private_key" "deploy" {
|
||||||
|
algorithm = "ED25519"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "hcloud_ssh_key" "default" {
|
||||||
|
name = "writekit"
|
||||||
|
public_key = var.ssh_public_key
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "hcloud_network" "writekit" {
|
||||||
|
name = "writekit-internal"
|
||||||
|
ip_range = "10.0.0.0/16"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "hcloud_network_subnet" "writekit" {
|
||||||
|
network_id = hcloud_network.writekit.id
|
||||||
|
type = "cloud"
|
||||||
|
network_zone = "eu-central"
|
||||||
|
ip_range = "10.0.0.0/24"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "hcloud_server" "prod" {
|
||||||
|
name = "writekit-prod"
|
||||||
|
image = "ubuntu-24.04"
|
||||||
|
server_type = var.prod_server_type
|
||||||
|
location = var.location
|
||||||
|
ssh_keys = [hcloud_ssh_key.default.id]
|
||||||
|
|
||||||
|
user_data = templatefile("${path.module}/files/cloud-init/prod.yml", {
|
||||||
|
ssh_public_key = var.ssh_public_key
|
||||||
|
deploy_ssh_public_key = tls_private_key.deploy.public_key_openssh
|
||||||
|
env_file = local.prod_env_file
|
||||||
|
docker_compose = local.prod_compose
|
||||||
|
})
|
||||||
|
|
||||||
|
public_net {
|
||||||
|
ipv4_enabled = true
|
||||||
|
ipv6_enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
environment = "production"
|
||||||
|
managed_by = "terraform"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "hcloud_server_network" "prod" {
|
||||||
|
server_id = hcloud_server.prod.id
|
||||||
|
network_id = hcloud_network.writekit.id
|
||||||
|
ip = "10.0.0.2"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "hcloud_firewall" "prod" {
|
||||||
|
name = "writekit-prod"
|
||||||
|
|
||||||
|
rule {
|
||||||
|
direction = "in"
|
||||||
|
protocol = "tcp"
|
||||||
|
port = "22"
|
||||||
|
source_ips = var.allowed_ssh_ips
|
||||||
|
}
|
||||||
|
|
||||||
|
rule {
|
||||||
|
direction = "in"
|
||||||
|
protocol = "tcp"
|
||||||
|
port = "22"
|
||||||
|
source_ips = ["10.0.0.0/24"]
|
||||||
|
}
|
||||||
|
|
||||||
|
rule {
|
||||||
|
direction = "in"
|
||||||
|
protocol = "tcp"
|
||||||
|
port = "80"
|
||||||
|
source_ips = ["0.0.0.0/0", "::/0"]
|
||||||
|
}
|
||||||
|
|
||||||
|
rule {
|
||||||
|
direction = "in"
|
||||||
|
protocol = "tcp"
|
||||||
|
port = "443"
|
||||||
|
source_ips = ["0.0.0.0/0", "::/0"]
|
||||||
|
}
|
||||||
|
|
||||||
|
rule {
|
||||||
|
direction = "out"
|
||||||
|
protocol = "tcp"
|
||||||
|
port = "1-65535"
|
||||||
|
destination_ips = ["0.0.0.0/0", "::/0"]
|
||||||
|
}
|
||||||
|
|
||||||
|
rule {
|
||||||
|
direction = "out"
|
||||||
|
protocol = "udp"
|
||||||
|
port = "1-65535"
|
||||||
|
destination_ips = ["0.0.0.0/0", "::/0"]
|
||||||
|
}
|
||||||
|
|
||||||
|
rule {
|
||||||
|
direction = "out"
|
||||||
|
protocol = "icmp"
|
||||||
|
destination_ips = ["0.0.0.0/0", "::/0"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "hcloud_firewall_attachment" "prod" {
|
||||||
|
firewall_id = hcloud_firewall.prod.id
|
||||||
|
server_ids = [hcloud_server.prod.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "hcloud_server" "ops" {
|
||||||
|
name = "writekit-ops"
|
||||||
|
image = "ubuntu-24.04"
|
||||||
|
server_type = var.ops_server_type
|
||||||
|
location = var.location
|
||||||
|
ssh_keys = [hcloud_ssh_key.default.id]
|
||||||
|
|
||||||
|
user_data = templatefile("${path.module}/files/cloud-init/ops.yml", {
|
||||||
|
ssh_public_key = var.ssh_public_key
|
||||||
|
deploy_ssh_private_key = tls_private_key.deploy.private_key_openssh
|
||||||
|
env_file = local.ops_env_file
|
||||||
|
docker_compose = local.ops_compose
|
||||||
|
})
|
||||||
|
|
||||||
|
public_net {
|
||||||
|
ipv4_enabled = true
|
||||||
|
ipv6_enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
environment = "ops"
|
||||||
|
managed_by = "terraform"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "hcloud_server_network" "ops" {
|
||||||
|
server_id = hcloud_server.ops.id
|
||||||
|
network_id = hcloud_network.writekit.id
|
||||||
|
ip = "10.0.0.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "hcloud_firewall" "ops" {
|
||||||
|
name = "writekit-ops"
|
||||||
|
|
||||||
|
rule {
|
||||||
|
direction = "in"
|
||||||
|
protocol = "tcp"
|
||||||
|
port = "22"
|
||||||
|
source_ips = var.allowed_ssh_ips
|
||||||
|
}
|
||||||
|
|
||||||
|
rule {
|
||||||
|
direction = "in"
|
||||||
|
protocol = "tcp"
|
||||||
|
port = "80"
|
||||||
|
source_ips = ["0.0.0.0/0", "::/0"]
|
||||||
|
}
|
||||||
|
|
||||||
|
rule {
|
||||||
|
direction = "in"
|
||||||
|
protocol = "tcp"
|
||||||
|
port = "443"
|
||||||
|
source_ips = ["0.0.0.0/0", "::/0"]
|
||||||
|
}
|
||||||
|
|
||||||
|
rule {
|
||||||
|
direction = "in"
|
||||||
|
protocol = "tcp"
|
||||||
|
port = "2222"
|
||||||
|
source_ips = ["0.0.0.0/0", "::/0"]
|
||||||
|
}
|
||||||
|
|
||||||
|
rule {
|
||||||
|
direction = "in"
|
||||||
|
protocol = "tcp"
|
||||||
|
port = "5000"
|
||||||
|
source_ips = ["10.0.0.0/24"]
|
||||||
|
}
|
||||||
|
|
||||||
|
rule {
|
||||||
|
direction = "out"
|
||||||
|
protocol = "tcp"
|
||||||
|
port = "1-65535"
|
||||||
|
destination_ips = ["0.0.0.0/0", "::/0"]
|
||||||
|
}
|
||||||
|
|
||||||
|
rule {
|
||||||
|
direction = "out"
|
||||||
|
protocol = "udp"
|
||||||
|
port = "1-65535"
|
||||||
|
destination_ips = ["0.0.0.0/0", "::/0"]
|
||||||
|
}
|
||||||
|
|
||||||
|
rule {
|
||||||
|
direction = "out"
|
||||||
|
protocol = "icmp"
|
||||||
|
destination_ips = ["0.0.0.0/0", "::/0"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "hcloud_firewall_attachment" "ops" {
|
||||||
|
firewall_id = hcloud_firewall.ops.id
|
||||||
|
server_ids = [hcloud_server.ops.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "cloudflare_record" "root" {
|
||||||
|
zone_id = var.cloudflare_zone_id
|
||||||
|
name = "@"
|
||||||
|
content = hcloud_server.prod.ipv4_address
|
||||||
|
type = "A"
|
||||||
|
proxied = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "cloudflare_record" "wildcard" {
|
||||||
|
zone_id = var.cloudflare_zone_id
|
||||||
|
name = "*"
|
||||||
|
content = hcloud_server.prod.ipv4_address
|
||||||
|
type = "A"
|
||||||
|
proxied = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "cloudflare_record" "staging" {
|
||||||
|
zone_id = var.cloudflare_zone_id
|
||||||
|
name = "staging"
|
||||||
|
content = hcloud_server.prod.ipv4_address
|
||||||
|
type = "A"
|
||||||
|
proxied = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "cloudflare_record" "staging_wildcard" {
|
||||||
|
zone_id = var.cloudflare_zone_id
|
||||||
|
name = "*.staging"
|
||||||
|
content = hcloud_server.prod.ipv4_address
|
||||||
|
type = "A"
|
||||||
|
proxied = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "cloudflare_record" "source" {
|
||||||
|
zone_id = var.cloudflare_zone_id
|
||||||
|
name = "source"
|
||||||
|
content = hcloud_server.ops.ipv4_address
|
||||||
|
type = "A"
|
||||||
|
proxied = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "cloudflare_record" "ci" {
|
||||||
|
zone_id = var.cloudflare_zone_id
|
||||||
|
name = "ci"
|
||||||
|
content = hcloud_server.ops.ipv4_address
|
||||||
|
type = "A"
|
||||||
|
proxied = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "cloudflare_r2_bucket" "assets" {
|
||||||
|
account_id = var.cloudflare_account_id
|
||||||
|
name = "writekit-assets"
|
||||||
|
location = "WEUR"
|
||||||
|
}
|
||||||
20
outputs.tf
Normal file
20
outputs.tf
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
output "prod_ip" {
|
||||||
|
value = hcloud_server.prod.ipv4_address
|
||||||
|
}
|
||||||
|
|
||||||
|
output "ops_ip" {
|
||||||
|
value = hcloud_server.ops.ipv4_address
|
||||||
|
}
|
||||||
|
|
||||||
|
output "urls" {
|
||||||
|
value = {
|
||||||
|
app = "https://${var.domain}"
|
||||||
|
staging = "https://staging.${var.domain}"
|
||||||
|
source = "https://source.${var.domain}"
|
||||||
|
ci = "https://ci.${var.domain}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "r2_bucket" {
|
||||||
|
value = cloudflare_r2_bucket.assets.name
|
||||||
|
}
|
||||||
13
terraform.tfvars.example
Normal file
13
terraform.tfvars.example
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
hcloud_token = ""
|
||||||
|
cloudflare_api_token = ""
|
||||||
|
cloudflare_zone_id = ""
|
||||||
|
cloudflare_account_id = ""
|
||||||
|
|
||||||
|
ssh_public_key = "ssh-ed25519 AAAA..."
|
||||||
|
|
||||||
|
location = "nbg1"
|
||||||
|
prod_server_type = "cx22"
|
||||||
|
ops_server_type = "cx22"
|
||||||
|
|
||||||
|
allowed_ssh_ips = ["YOUR_IP/32"]
|
||||||
|
domain = "writekit.dev"
|
||||||
45
variables.tf
Normal file
45
variables.tf
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
variable "hcloud_token" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "cloudflare_api_token" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "cloudflare_zone_id" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "cloudflare_account_id" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_public_key" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "location" {
|
||||||
|
type = string
|
||||||
|
default = "nbg1"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "prod_server_type" {
|
||||||
|
type = string
|
||||||
|
default = "cpx21"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ops_server_type" {
|
||||||
|
type = string
|
||||||
|
default = "cpx21"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "allowed_ssh_ips" {
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "domain" {
|
||||||
|
type = string
|
||||||
|
default = "writekit.dev"
|
||||||
|
}
|
||||||
26
versions.tf
Normal file
26
versions.tf
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
hcloud = {
|
||||||
|
source = "hetznercloud/hcloud"
|
||||||
|
version = "~> 1.58"
|
||||||
|
}
|
||||||
|
cloudflare = {
|
||||||
|
source = "cloudflare/cloudflare"
|
||||||
|
version = "~> 4.52"
|
||||||
|
}
|
||||||
|
tls = {
|
||||||
|
source = "hashicorp/tls"
|
||||||
|
version = "~> 4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "hcloud" {
|
||||||
|
token = var.hcloud_token
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "cloudflare" {
|
||||||
|
api_token = var.cloudflare_api_token
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue