sudo useradd --system --home $PROD_DIR --shell /usr/sbin/nologin $PROD_USER || true
sudo mkdir -p $PROD_DIR/{bin,frontend/dist,logs}
sudo chown -R $PROD_USER:$PROD_USER $PROD_DIR
sudo tee $PROD_DIR/.env >/dev/null <<'EOF'
APP_ENV=production
RUST_LOG=info
APP_HOST=127.0.0.1
APP_PORT=8080
DATABASE_URL=postgres://user:pass@dbhost:5432/dbname?sslmode=require
EOF
sudo chown $PROD_USER:$PROD_USER $PROD_DIR/.env
sudo chmod 640 $PROD_DIR/.env
sudo tee /etc/systemd/system/$APP_NAME.service >/dev/null <<EOF
[Unit]
Description=$APP_NAME Rust Service (static MUSL)
After=network-online.target
Wants=network-online.target
[Service]
User=$PROD_USER
Group=$PROD_USER
EnvironmentFile=$PROD_DIR/.env
WorkingDirectory=$PROD_DIR
ExecStart=$PROD_DIR/bin/$APP_NAME
Restart=always
RestartSec=2
StandardOutput=append:$PROD_DIR/logs/$APP_NAME.out.log
StandardError=append:$PROD_DIR/logs/$APP_NAME.err.log
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ProtectHostname=true
ProtectClock=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
LockPersonality=true
MemoryDenyWriteExecute=true
ReadWritePaths=$PROD_DIR
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now $APP_NAME
sudo systemctl status $APP_NAME --no-pager
server {
listen 80;
server_name _;
root /opt/magictoybox_server_generic/frontend/dist;
index index.html;
location / { try_files $uri $uri/ /index.html; }
location /api/ {
proxy_pass http://127.0.0.1:8080/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
sudo ln -s /etc/nginx/sites-available/magictoybox /etc/nginx/sites-enabled/magictoybox
sudo nginx -t
sudo systemctl reload nginx
rustup target add x86_64-unknown-linux-musl
sudo apt-get update && sudo apt-get install -y musl-tools
cd $DEV_DIR/server
RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --locked --target x86_64-unknown-linux-musl
strip target/x86_64-unknown-linux-musl/release/$APP_NAME || true
cd $DEV_DIR/frontend
npm ci && npm run build
Artifacts:
server/target/x86_64-unknown-linux-musl/release/magictoybox_server_genericfrontend/dist/cd $DEV_DIR
REL_OUT=/tmp/${APP_NAME}_release
rm -rf "$REL_OUT" && mkdir -p "$REL_OUT"
cp server/target/x86_64-unknown-linux-musl/release/$APP_NAME "$REL_OUT"/
rsync -a frontend/dist/ "$REL_OUT/dist/"
tar -C /tmp -czf /tmp/${APP_NAME}_$(date +%Y%m%d%H%M%S).tar.gz ${APP_NAME}_release
scp $DEV_DIR/server/target/x86_64-unknown-linux-musl/release/$APP_NAME $PROD_HOST:$PROD_DIR/bin/$APP_NAME
rsync -avzP --delete $DEV_DIR/frontend/dist/ $PROD_HOST:$PROD_DIR/frontend/dist/
ssh $PROD_HOST "sudo systemctl restart $APP_NAME && sudo systemctl reload nginx"
ssh $PROD_HOST "ss -ltnp | grep $RUST_PORT || true"
ssh $PROD_HOST "journalctl -u $APP_NAME -n 100 --no-pager"
curl -I http://$PROD_HOST/
curl -i http://$PROD_HOST/api/health
ssh $PROD_HOST "ls -1 /opt/$APP_NAME/releases"
ssh $PROD_HOST bash -lc '
APP_NAME=magictoybox_server_generic
BASE=/opt/$APP_NAME
PREV=$(ls -1 $BASE/releases | sort | tail -n 2 | head -n 1)
ln -sfn $BASE/releases/$PREV $BASE/current
ln -sfn $BASE/current/bin/$APP_NAME $BASE/bin/$APP_NAME
ln -sfn $BASE/current/frontend/dist $BASE/frontend/dist
sudo systemctl restart $APP_NAME && sudo systemctl reload nginx
'
name: build-musl
on:
push:
branches: [ main ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-musl
- run: sudo apt-get update && sudo apt-get install -y musl-tools
- name: Cargo build
working-directory: server
env:
RUSTFLAGS: "-C target-feature=+crt-static"
run: cargo build --release --locked --target x86_64-unknown-linux-musl
- run: strip server/target/x86_64-unknown-linux-musl/release/magictoybox_server_generic || true
- name: Frontend build
working-directory: frontend
run: npm ci && npm run build
- name: Package artifact
run: |
mkdir -p /tmp/release/dist
cp server/target/x86_64-unknown-linux-musl/release/magictoybox_server_generic /tmp/release/
rsync -a frontend/dist/ /tmp/release/dist/
tar -C /tmp -czf /tmp/magictoybox_release_${{ github.sha }}.tar.gz release
- uses: actions/upload-artifact@v4
with:
name: magictoybox_release_${{ github.sha }}
path: /tmp/magictoybox_release_${{ github.sha }}.tar.gz
server/target/x86_64-unknown-linux-musl/release/magictoybox_server_genericfrontend/dist/[ ] MUSL target installed
[ ] Binary builds
[ ] Frontend builds
[ ] PROD .env created
[ ] systemd active
[ ] Nginx proxy working
[ ] SSH deploy tested
[ ] Health endpoint reachable
[ ] Rollback validated