Skip to content

Commit fc74049

Browse files
committed
Add production public deploy URLs and nginx app routing
Deployed apps get https://{project}-{user8}.rustapi.tunayinbayramharciligi.com with per-app nginx map entries. DEPLOY_PUBLIC_HOST, NGINX_DEPLOY_MAP_DIR config.
1 parent 930b91b commit fc74049

13 files changed

Lines changed: 570 additions & 22 deletions

RustAPI-Cloud/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@ GITHUB_CLIENT_ID=Ov23li...
66
GITHUB_CLIENT_SECRET=...
77
GITHUB_REDIRECT_URI=http://localhost:3000/auth/callback
88
STORAGE_ROOT=./storage
9+
DEPLOY_PUBLIC_HOST=rustapi.tunayinbayramharciligi.com
10+
DEPLOY_URL_SCHEME=https
11+
NGINX_DEPLOY_MAP_DIR=/etc/nginx/rustapi-deploy-map.d
912
RUST_LOG=rustapi_cloud=debug,info

RustAPI-Cloud/deploy/install.sh

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/usr/bin/env bash
2+
# RustAPI Cloud — production install on Ubuntu/Debian
3+
# Usage: DOMAIN=api.example.com JWT_SECRET=... GITHUB_CLIENT_ID=... GITHUB_CLIENT_SECRET=... ./install.sh
4+
set -euo pipefail
5+
6+
DOMAIN="${DOMAIN:-rustapi.tunayinbayramharciligi.com}"
7+
APP_ROOT="${APP_ROOT:-/opt/rustapi}"
8+
CLOUD_DIR="${APP_ROOT}/RustAPI-Cloud"
9+
SERVICE_USER="${SERVICE_USER:-rustapi}"
10+
STORAGE_ROOT="/var/lib/rustapi-cloud/storage"
11+
NGINX_MAP_DIR="/etc/nginx/rustapi-deploy-map.d"
12+
13+
echo "==> RustAPI Cloud install (domain: ${DOMAIN})"
14+
15+
if [[ "$(id -u)" -ne 0 ]]; then
16+
echo "Run as root." >&2
17+
exit 1
18+
fi
19+
20+
echo "==> Remove previous release"
21+
systemctl stop rustapi-cloud 2>/dev/null || true
22+
systemctl disable rustapi-cloud 2>/dev/null || true
23+
rm -f /etc/systemd/system/rustapi-cloud.service
24+
rm -f /etc/nginx/sites-enabled/rustapi-cloud /etc/nginx/sites-available/rustapi-cloud
25+
rm -f /etc/nginx/sites-enabled/rustapi-apps /etc/nginx/sites-available/rustapi-apps
26+
rm -rf "${NGINX_MAP_DIR}"
27+
rm -rf /opt/rustapi-cloud /var/www/rustapi-cloud /srv/rustapi-cloud
28+
docker rm -f rustapi-cloud-db 2>/dev/null || true
29+
30+
echo "==> Install system packages"
31+
export DEBIAN_FRONTEND=noninteractive
32+
apt-get update -qq
33+
apt-get install -y -qq \
34+
ca-certificates curl git build-essential pkg-config libssl-dev \
35+
nginx certbot python3-certbot-nginx \
36+
docker.io docker-compose-plugin postgresql-client
37+
38+
systemctl enable --now docker
39+
systemctl enable --now nginx
40+
41+
if ! id "${SERVICE_USER}" &>/dev/null; then
42+
useradd --system --home "${APP_ROOT}" --shell /usr/sbin/nologin "${SERVICE_USER}"
43+
fi
44+
45+
if ! command -v cargo &>/dev/null; then
46+
echo "==> Install Rust toolchain"
47+
su - "${SERVICE_USER}" -c 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y'
48+
fi
49+
50+
if [[ ! -f "${CLOUD_DIR}/Cargo.toml" ]]; then
51+
echo "==> Clone repository (or upload RustAPI tree to ${APP_ROOT} first)"
52+
mkdir -p "${APP_ROOT}"
53+
git clone --depth 1 https://github.com/Tuntii/RustAPI.git "${APP_ROOT}"
54+
fi
55+
chown -R "${SERVICE_USER}:${SERVICE_USER}" "${APP_ROOT}"
56+
57+
cd "${CLOUD_DIR}"
58+
59+
echo "==> Start Postgres"
60+
docker compose up -d
61+
for i in $(seq 1 30); do
62+
if docker compose exec -T db pg_isready -U rustapi -d rustapi_cloud &>/dev/null; then
63+
break
64+
fi
65+
sleep 2
66+
done
67+
68+
echo "==> Apply migrations"
69+
for f in migrations/*.sql; do
70+
echo " - $(basename "$f")"
71+
docker compose exec -T db psql -U rustapi -d rustapi_cloud -v ON_ERROR_STOP=1 < "$f"
72+
done
73+
74+
JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32)}"
75+
GITHUB_CLIENT_ID="${GITHUB_CLIENT_ID:-REPLACE_ME}"
76+
GITHUB_CLIENT_SECRET="${GITHUB_CLIENT_SECRET:-REPLACE_ME}"
77+
78+
mkdir -p "${STORAGE_ROOT}"
79+
chown -R "${SERVICE_USER}:${SERVICE_USER}" "${STORAGE_ROOT}"
80+
81+
cat > "${CLOUD_DIR}/.env" <<EOF
82+
DATABASE_URL=postgres://rustapi:rustapi@localhost:5432/rustapi_cloud
83+
HOST=127.0.0.1
84+
PORT=3000
85+
JWT_SECRET=${JWT_SECRET}
86+
GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
87+
GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
88+
GITHUB_REDIRECT_URI=https://${DOMAIN}/auth/callback
89+
STORAGE_ROOT=${STORAGE_ROOT}
90+
DEPLOY_PUBLIC_HOST=${DOMAIN}
91+
DEPLOY_URL_SCHEME=https
92+
NGINX_DEPLOY_MAP_DIR=${NGINX_MAP_DIR}
93+
RUST_LOG=rustapi_cloud=info,info
94+
EOF
95+
chmod 600 "${CLOUD_DIR}/.env"
96+
chown "${SERVICE_USER}:${SERVICE_USER}" "${CLOUD_DIR}/.env"
97+
98+
echo "==> Build rustapi-cloud (release)"
99+
su - "${SERVICE_USER}" -c "cd ${CLOUD_DIR} && source \$HOME/.cargo/env && cargo build --release"
100+
101+
install -m 0644 "${CLOUD_DIR}/deploy/rustapi-cloud.service" /etc/systemd/system/rustapi-cloud.service
102+
sed -i "s|__APP_ROOT__|${APP_ROOT}|g" /etc/systemd/system/rustapi-cloud.service
103+
sed -i "s|__SERVICE_USER__|${SERVICE_USER}|g" /etc/systemd/system/rustapi-cloud.service
104+
105+
mkdir -p "${NGINX_MAP_DIR}"
106+
chown "${SERVICE_USER}:${SERVICE_USER}" "${NGINX_MAP_DIR}"
107+
chmod 0755 "${NGINX_MAP_DIR}"
108+
echo "${SERVICE_USER} ALL=(root) NOPASSWD: /usr/sbin/nginx -s reload" > /etc/sudoers.d/rustapi-nginx-reload
109+
chmod 0440 /etc/sudoers.d/rustapi-nginx-reload
110+
111+
install -m 0644 "${CLOUD_DIR}/deploy/nginx-rustapi-cloud.conf" /etc/nginx/sites-available/rustapi-cloud
112+
sed -i "s|__DOMAIN__|${DOMAIN}|g" /etc/nginx/sites-available/rustapi-cloud
113+
ln -sf /etc/nginx/sites-available/rustapi-cloud /etc/nginx/sites-enabled/rustapi-cloud
114+
115+
install -m 0644 "${CLOUD_DIR}/deploy/nginx-rustapi-apps.conf" /etc/nginx/sites-available/rustapi-apps
116+
sed -i "s|__DOMAIN__|${DOMAIN}|g" /etc/nginx/sites-available/rustapi-apps
117+
sed -i "s|__NGINX_MAP_DIR__|${NGINX_MAP_DIR}|g" /etc/nginx/sites-available/rustapi-apps
118+
ln -sf /etc/nginx/sites-available/rustapi-apps /etc/nginx/sites-enabled/rustapi-apps
119+
rm -f /etc/nginx/sites-enabled/default
120+
nginx -t
121+
systemctl reload nginx
122+
123+
systemctl daemon-reload
124+
systemctl enable --now rustapi-cloud
125+
126+
if [[ "${GITHUB_CLIENT_ID}" == "REPLACE_ME" ]]; then
127+
echo "WARN: Set real GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET in ${CLOUD_DIR}/.env then: systemctl restart rustapi-cloud"
128+
fi
129+
130+
if certbot certificates 2>/dev/null | grep -q "${DOMAIN}"; then
131+
certbot renew --quiet || true
132+
else
133+
echo "==> Obtain TLS certificate (requires DNS A record -> this server)"
134+
certbot --nginx -d "${DOMAIN}" --non-interactive --agree-tos -m "admin@${DOMAIN#*.}" --redirect || \
135+
echo "WARN: certbot failed — point ${DOMAIN} A record to this server, then run: certbot --nginx -d ${DOMAIN}"
136+
certbot --nginx -d "*.${DOMAIN}" --non-interactive --agree-tos -m "admin@${DOMAIN#*.}" --redirect 2>/dev/null || \
137+
echo "WARN: wildcard TLS for *.${DOMAIN} needs DNS-01 — run: certbot certonly --manual --preferred-challenges dns -d '*.${DOMAIN}'"
138+
fi
139+
140+
echo "==> Health check"
141+
sleep 3
142+
curl -fsS "http://127.0.0.1:3000/health" && echo
143+
echo "DONE: https://${DOMAIN}/health"
144+
echo "User apps: https://{project}-{user8}.${DOMAIN}"
145+
echo "DNS: A ${DOMAIN} -> server IP, A *.${DOMAIN} -> server IP (wildcard)"
146+
echo "CLI login: cargo rustapi login --cloud-url https://${DOMAIN}"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Wildcard vhost for user-deployed apps (*.rustapi.example.com).
2+
# Per-app port mappings are written to __NGINX_MAP_DIR__/*.conf by rustapi-cloud.
3+
4+
map $host $rustapi_deploy_port {
5+
default 0;
6+
include __NGINX_MAP_DIR__/*.conf;
7+
}
8+
9+
server {
10+
listen 80;
11+
listen [::]:80;
12+
server_name *.__DOMAIN__;
13+
14+
client_max_body_size 100M;
15+
16+
location / {
17+
if ($rustapi_deploy_port = 0) {
18+
return 404;
19+
}
20+
proxy_pass http://127.0.0.1:$rustapi_deploy_port;
21+
proxy_http_version 1.1;
22+
proxy_set_header Host $host;
23+
proxy_set_header X-Real-IP $remote_addr;
24+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
25+
proxy_set_header X-Forwarded-Proto $scheme;
26+
proxy_read_timeout 300s;
27+
}
28+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
server {
2+
listen 80;
3+
listen [::]:80;
4+
server_name __DOMAIN__;
5+
6+
client_max_body_size 100M;
7+
8+
location / {
9+
proxy_pass http://127.0.0.1:3000;
10+
proxy_http_version 1.1;
11+
proxy_set_header Host $host;
12+
proxy_set_header X-Real-IP $remote_addr;
13+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
14+
proxy_set_header X-Forwarded-Proto $scheme;
15+
proxy_read_timeout 300s;
16+
}
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[Unit]
2+
Description=RustAPI Cloud API
3+
After=network.target docker.service
4+
Wants=docker.service
5+
6+
[Service]
7+
Type=simple
8+
User=__SERVICE_USER__
9+
Group=__SERVICE_USER__
10+
WorkingDirectory=__APP_ROOT__/RustAPI-Cloud
11+
EnvironmentFile=__APP_ROOT__/RustAPI-Cloud/.env
12+
ExecStart=__APP_ROOT__/RustAPI-Cloud/target/release/rustapi-cloud
13+
Restart=on-failure
14+
RestartSec=5
15+
16+
[Install]
17+
WantedBy=multi-user.target

RustAPI-Cloud/src/bin/verify-deploy-e2e.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ async fn main() -> anyhow::Result<()> {
110110
github_client_secret: "test".into(),
111111
github_redirect_uri: format!("http://127.0.0.1:{port}/auth/callback"),
112112
storage_root: storage.to_string_lossy().into(),
113+
deploy: rustapi_cloud::config::DeploySettings::default(),
113114
};
114115

115116
let pool = create_pool(&database_url).await;

RustAPI-Cloud/src/config.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
use dotenvy::dotenv;
22
use std::env;
33

4+
/// Production deploy routing (public URLs + nginx map files for user apps).
5+
#[derive(Clone, Debug, Default)]
6+
pub struct DeploySettings {
7+
/// Base host for deployed apps, e.g. `rustapi.tunayinbayramharciligi.com`.
8+
/// Apps are served at `{project}-{user8}.{public_host}`.
9+
pub public_host: Option<String>,
10+
pub url_scheme: String,
11+
/// Directory of per-app `.conf` map snippets included by nginx wildcard vhost.
12+
pub nginx_map_dir: Option<String>,
13+
}
14+
415
#[derive(Clone)]
516
pub struct Config {
617
pub database_url: String,
@@ -11,6 +22,7 @@ pub struct Config {
1122
pub github_client_secret: String,
1223
pub github_redirect_uri: String,
1324
pub storage_root: String,
25+
pub deploy: DeploySettings,
1426
}
1527

1628
impl Config {
@@ -31,6 +43,15 @@ impl Config {
3143
github_redirect_uri: env::var("GITHUB_REDIRECT_URI")
3244
.expect("GITHUB_REDIRECT_URI must be set"),
3345
storage_root: env::var("STORAGE_ROOT").unwrap_or_else(|_| "./storage".into()),
46+
deploy: DeploySettings {
47+
public_host: env::var("DEPLOY_PUBLIC_HOST")
48+
.ok()
49+
.filter(|value| !value.trim().is_empty()),
50+
url_scheme: env::var("DEPLOY_URL_SCHEME").unwrap_or_else(|_| "https".into()),
51+
nginx_map_dir: env::var("NGINX_DEPLOY_MAP_DIR")
52+
.ok()
53+
.filter(|value| !value.trim().is_empty()),
54+
},
3455
}
3556
}
3657
}

0 commit comments

Comments
 (0)