mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 10:10:55 +00:00
update to hetzner deployment (#220)
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
#!/bin/bash
|
||||
# deploy.sh - Complete deployment script for Hetzner with Docker Hub and R2
|
||||
# This script:
|
||||
# 1. Builds and uploads the Docker image to Docker Hub with appropriate tag
|
||||
# 2. Copies the update script to Hetzner server
|
||||
# 3. Executes the update script on the Hetzner server
|
||||
|
||||
set -e # Exit immediately if a command exits with a non-zero status
|
||||
|
||||
# Function to print section headers
|
||||
print_header() {
|
||||
echo "======================================================"
|
||||
echo "🚀 $1"
|
||||
echo "======================================================"
|
||||
}
|
||||
|
||||
# Load environment variables
|
||||
if [ -f .env ]; then
|
||||
echo "Loading configuration from .env file..."
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
fi
|
||||
|
||||
# Check command line argument
|
||||
if [ $# -ne 1 ] || ([ "$1" != "staging" ] && [ "$1" != "prod" ]); then
|
||||
echo "Error: Please specify environment (staging or prod)"
|
||||
echo "Usage: $0 [staging|prod]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# TODO: fix this - need to build before creating the image
|
||||
bun run build-prod
|
||||
|
||||
ENV=$1
|
||||
VERSION_TAG="latest"
|
||||
DOCKER_REPO=""
|
||||
|
||||
# Set environment-specific variables
|
||||
if [ "$ENV" == "staging" ]; then
|
||||
print_header "DEPLOYING TO STAGING ENVIRONMENT"
|
||||
SERVER_HOST=$SERVER_HOST_STAGING
|
||||
DOCKER_REPO=$DOCKER_REPO_STAGING
|
||||
else
|
||||
print_header "DEPLOYING TO PRODUCTION ENVIRONMENT"
|
||||
SERVER_HOST=$SERVER_HOST_PROD
|
||||
DOCKER_REPO=$DOCKER_REPO_PROD
|
||||
fi
|
||||
|
||||
# Check required environment variables
|
||||
if [ -z "$SERVER_HOST" ]; then
|
||||
echo "Error: SERVER_HOST_${ENV^^} not defined in .env file or environment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configuration
|
||||
SSH_KEY=${SSH_KEY:-"~/.ssh/id_rsa"} # Use default or override from .env
|
||||
DOCKER_USERNAME=${DOCKER_USERNAME} # Docker Hub username
|
||||
UPDATE_SCRIPT="./update.sh" # Path to your update script
|
||||
REMOTE_UPDATE_SCRIPT="/root/update-openfront.sh" # Where to place the script on server
|
||||
|
||||
# Check if update script exists
|
||||
if [ ! -f "$UPDATE_SCRIPT" ]; then
|
||||
echo "Error: Update script $UPDATE_SCRIPT not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 1: Build and upload Docker image to Docker Hub
|
||||
print_header "STEP 1: Building and uploading Docker image to Docker Hub"
|
||||
echo "Environment: ${ENV}"
|
||||
echo "Using version tag: $VERSION_TAG"
|
||||
echo "Docker repository: $DOCKER_REPO"
|
||||
|
||||
# Get Git commit for build info
|
||||
GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
||||
echo "Git commit: $GIT_COMMIT"
|
||||
|
||||
docker buildx build \
|
||||
--no-cache \
|
||||
--platform linux/amd64 \
|
||||
--build-arg GIT_COMMIT=$GIT_COMMIT \
|
||||
-t $DOCKER_USERNAME/$DOCKER_REPO:$VERSION_TAG \
|
||||
--push \
|
||||
.
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Docker build failed. Stopping deployment."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Failed to push image to Docker Hub. Stopping deployment."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Docker image built and pushed successfully."
|
||||
|
||||
# Step 2: Copy update script to Hetzner server
|
||||
print_header "STEP 2: Copying update script to server"
|
||||
echo "Target: $SERVER_HOST"
|
||||
|
||||
# Make sure the update script is executable
|
||||
chmod +x $UPDATE_SCRIPT
|
||||
|
||||
# Copy the update script to the server
|
||||
scp -i $SSH_KEY $UPDATE_SCRIPT $SERVER_HOST:$REMOTE_UPDATE_SCRIPT
|
||||
|
||||
# Copy environment variables if needed
|
||||
if [ -f .env ]; then
|
||||
scp -i $SSH_KEY .env $SERVER_HOST:/root/.env
|
||||
# Secure the .env file
|
||||
ssh -i $SSH_KEY $SERVER_HOST "chmod 600 /root/.env"
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Failed to copy update script to server. Stopping deployment."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Update script successfully copied to server."
|
||||
|
||||
# Step 3: Execute the update script on the server
|
||||
print_header "STEP 3: Executing update script on server"
|
||||
|
||||
# Make the script executable on the remote server and execute it with the environment parameter
|
||||
ssh -i $SSH_KEY $SERVER_HOST "chmod +x $REMOTE_UPDATE_SCRIPT && $REMOTE_UPDATE_SCRIPT $ENV $DOCKER_USERNAME $DOCKER_REPO"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Failed to execute update script on server."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_header "DEPLOYMENT COMPLETED SUCCESSFULLY"
|
||||
echo "✅ New version deployed to ${ENV} environment!"
|
||||
echo "🌐 Check your ${ENV} server to verify the deployment."
|
||||
echo "======================================================="
|
||||
@@ -102,6 +102,10 @@ export interface ServerConfig {
|
||||
adminHeader(): string;
|
||||
// Only available on the server
|
||||
gitCommit(): string;
|
||||
r2Bucket(): string;
|
||||
r2Endpoint(): string;
|
||||
r2AccessKey(): string;
|
||||
r2SecretKey(): string;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
|
||||
@@ -25,6 +25,16 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
gitCommit(): string {
|
||||
return process.env.GIT_COMMIT;
|
||||
}
|
||||
r2Endpoint(): string {
|
||||
return process.env.R2_ENDPOINT;
|
||||
}
|
||||
r2AccessKey(): string {
|
||||
return process.env.R2_ACCESS_KEY;
|
||||
}
|
||||
r2SecretKey(): string {
|
||||
return process.env.R2_SECRET_KEY;
|
||||
}
|
||||
abstract r2Bucket(): string;
|
||||
adminHeader(): string {
|
||||
return "x-admin-key";
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import { GameEnv, ServerConfig } from "./Config";
|
||||
import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig";
|
||||
|
||||
export class DevServerConfig extends DefaultServerConfig {
|
||||
r2Bucket(): string {
|
||||
return "openfront-staging";
|
||||
}
|
||||
adminToken(): string {
|
||||
return "WARNING_DEV_ADMIN_KEY_DO_NOT_USE_IN_PRODUCTION";
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import { GameEnv } from "./Config";
|
||||
import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig";
|
||||
|
||||
export const preprodConfig = new (class extends DefaultServerConfig {
|
||||
r2Bucket(): string {
|
||||
return "openfront-staging";
|
||||
}
|
||||
env(): GameEnv {
|
||||
return GameEnv.Preprod;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import { GameEnv } from "./Config";
|
||||
import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig";
|
||||
|
||||
export const prodConfig = new (class extends DefaultServerConfig {
|
||||
r2Bucket(): string {
|
||||
return "openfront-prod";
|
||||
}
|
||||
numWorkers(): number {
|
||||
return 6;
|
||||
}
|
||||
|
||||
+36
-28
@@ -1,6 +1,5 @@
|
||||
import { GameRecord, GameID, GameRecordSchema } from "../core/Schemas";
|
||||
import { S3 } from "@aws-sdk/client-s3";
|
||||
import { RedshiftData } from "@aws-sdk/client-redshift-data";
|
||||
import {
|
||||
GameEnv,
|
||||
getServerConfigFromServer,
|
||||
@@ -8,23 +7,32 @@ import {
|
||||
|
||||
const config = getServerConfigFromServer();
|
||||
|
||||
const s3 = new S3({ region: "eu-west-1" });
|
||||
// R2 client configuration
|
||||
const r2 = new S3({
|
||||
region: "auto", // R2 ignores region, but it's required by the SDK
|
||||
endpoint: config.r2Endpoint(), // You'll need to add this to your config
|
||||
credentials: {
|
||||
accessKeyId: config.r2AccessKey(), // You'll need to add these
|
||||
secretAccessKey: config.r2SecretKey(), // credential methods to your config
|
||||
},
|
||||
});
|
||||
|
||||
const gameBucket = "openfront-games";
|
||||
const analyticsBucket = "openfront-analytics";
|
||||
const bucket = config.r2Bucket();
|
||||
const gameFolder = "games";
|
||||
const analyticsFolder = "analytics";
|
||||
|
||||
export async function archive(gameRecord: GameRecord) {
|
||||
try {
|
||||
gameRecord.gitCommit = config.gitCommit();
|
||||
// Archive to Redshift Serverless
|
||||
await archiveAnalyticsToS3(gameRecord);
|
||||
// Archive to R2
|
||||
await archiveAnalyticsToR2(gameRecord);
|
||||
|
||||
// Archive to S3 if there are turns
|
||||
// Archive full game if there are turns
|
||||
if (gameRecord.turns.length > 0) {
|
||||
console.log(
|
||||
`${gameRecord.id}: game has more than zero turns, attempting to write to full game to S3`,
|
||||
`${gameRecord.id}: game has more than zero turns, attempting to write to full game to R2`,
|
||||
);
|
||||
await archiveFullGameToS3(gameRecord);
|
||||
await archiveFullGameToR2(gameRecord);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${gameRecord.id}: Final archive error: ${error}`, {
|
||||
@@ -36,8 +44,8 @@ export async function archive(gameRecord: GameRecord) {
|
||||
}
|
||||
}
|
||||
|
||||
async function archiveAnalyticsToS3(gameRecord: GameRecord) {
|
||||
// Create analytics data object (similar to what was going to Redshift)
|
||||
async function archiveAnalyticsToR2(gameRecord: GameRecord) {
|
||||
// Create analytics data object
|
||||
const analyticsData = {
|
||||
id: gameRecord.id,
|
||||
env: config.env(),
|
||||
@@ -61,17 +69,17 @@ async function archiveAnalyticsToS3(gameRecord: GameRecord) {
|
||||
// Store analytics data using just the game ID as the key
|
||||
const analyticsKey = `${gameRecord.id}.json`;
|
||||
|
||||
await s3.putObject({
|
||||
Bucket: analyticsBucket,
|
||||
Key: analyticsKey,
|
||||
await r2.putObject({
|
||||
Bucket: bucket,
|
||||
Key: `${analyticsFolder}/${analyticsKey}`,
|
||||
Body: JSON.stringify(analyticsData),
|
||||
ContentType: "application/json",
|
||||
});
|
||||
|
||||
console.log(`${gameRecord.id}: successfully wrote game analytics to S3`);
|
||||
console.log(`${gameRecord.id}: successfully wrote game analytics to R2`);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`${gameRecord.id}: Error writing game analytics to S3: ${error}`,
|
||||
`${gameRecord.id}: Error writing game analytics to R2: ${error}`,
|
||||
{
|
||||
message: error?.message || error,
|
||||
stack: error?.stack,
|
||||
@@ -83,7 +91,7 @@ async function archiveAnalyticsToS3(gameRecord: GameRecord) {
|
||||
}
|
||||
}
|
||||
|
||||
async function archiveFullGameToS3(gameRecord: GameRecord) {
|
||||
async function archiveFullGameToR2(gameRecord: GameRecord) {
|
||||
// Create a deep copy to avoid modifying the original
|
||||
const recordCopy = JSON.parse(JSON.stringify(gameRecord));
|
||||
|
||||
@@ -94,9 +102,9 @@ async function archiveFullGameToS3(gameRecord: GameRecord) {
|
||||
});
|
||||
|
||||
try {
|
||||
await s3.putObject({
|
||||
Bucket: gameBucket,
|
||||
Key: recordCopy.id,
|
||||
await r2.putObject({
|
||||
Bucket: bucket,
|
||||
Key: `${gameFolder}/${recordCopy.id}`,
|
||||
Body: JSON.stringify(recordCopy),
|
||||
ContentType: "application/json",
|
||||
});
|
||||
@@ -105,7 +113,7 @@ async function archiveFullGameToS3(gameRecord: GameRecord) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log(`${gameRecord.id}: game record successfully written to S3`);
|
||||
console.log(`${gameRecord.id}: game record successfully written to R2`);
|
||||
}
|
||||
|
||||
export async function readGameRecord(
|
||||
@@ -113,16 +121,16 @@ export async function readGameRecord(
|
||||
): Promise<GameRecord | null> {
|
||||
try {
|
||||
// Check if file exists and download in one operation
|
||||
const response = await s3.getObject({
|
||||
Bucket: gameBucket,
|
||||
Key: gameId,
|
||||
const response = await r2.getObject({
|
||||
Bucket: bucket,
|
||||
Key: `${gameFolder}/${gameId}`, // Fixed - needed to include gameFolder
|
||||
});
|
||||
// Parse the response body
|
||||
const bodyContents = await response.Body.transformToString();
|
||||
return JSON.parse(bodyContents) as GameRecord;
|
||||
} catch (error) {
|
||||
// Log the error for monitoring purposes
|
||||
console.error(`${gameId}: Error reading game record from S3: ${error}`, {
|
||||
console.error(`${gameId}: Error reading game record from R2: ${error}`, {
|
||||
message: error?.message || error,
|
||||
stack: error?.stack,
|
||||
name: error?.name,
|
||||
@@ -136,9 +144,9 @@ export async function readGameRecord(
|
||||
|
||||
export async function gameRecordExists(gameId: GameID): Promise<boolean> {
|
||||
try {
|
||||
await s3.headObject({
|
||||
Bucket: gameBucket,
|
||||
Key: gameId,
|
||||
await r2.headObject({
|
||||
Bucket: bucket,
|
||||
Key: `${gameFolder}/${gameId}`, // Fixed - needed to include gameFolder
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
||||
@@ -11,18 +11,23 @@ fi
|
||||
# Set environment from parameter
|
||||
ENV=$1
|
||||
CONTAINER_NAME="openfront-${ENV}"
|
||||
LOG_GROUP="/aws/ec2/docker-containers/${ENV}"
|
||||
IMAGE_NAME="${DOCKER_USERNAME}/${DOCKER_REPO}"
|
||||
FULL_IMAGE_NAME="${IMAGE_NAME}:latest"
|
||||
|
||||
# Get AWS account ID
|
||||
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
|
||||
ECR_REPO="${AWS_ACCOUNT_ID}.dkr.ecr.eu-west-1.amazonaws.com/openfront:latest"
|
||||
echo "======================================================"
|
||||
echo "🔄 UPDATING SERVER: ${ENV} ENVIRONMENT"
|
||||
echo "======================================================"
|
||||
echo "Container name: ${CONTAINER_NAME}"
|
||||
echo "Docker image: ${FULL_IMAGE_NAME}"
|
||||
|
||||
echo "Deploying to ${ENV} environment..."
|
||||
echo "Logging in to ECR..."
|
||||
aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.eu-west-1.amazonaws.com
|
||||
# Load environment variables if .env exists
|
||||
if [ -f /root/.env ]; then
|
||||
echo "Loading environment variables from .env file..."
|
||||
export $(grep -v '^#' /root/.env | xargs)
|
||||
fi
|
||||
|
||||
echo "Pulling latest image..."
|
||||
docker pull $ECR_REPO
|
||||
echo "Pulling latest image from Docker Hub..."
|
||||
docker pull $FULL_IMAGE_NAME
|
||||
|
||||
echo "Checking for existing container..."
|
||||
# Check for running container
|
||||
@@ -64,14 +69,12 @@ fi
|
||||
echo "Starting new container for ${ENV} environment..."
|
||||
docker run -d -p 80:80 \
|
||||
--restart=always \
|
||||
--log-driver=awslogs \
|
||||
--log-opt awslogs-region=eu-west-1 \
|
||||
--log-opt awslogs-group=${LOG_GROUP} \
|
||||
--log-opt awslogs-create-group=true \
|
||||
$VOLUME_MOUNTS \
|
||||
$NETWORK_FLAGS \
|
||||
--env GAME_ENV=${ENV} \
|
||||
--env-file /home/ec2-user/.env \
|
||||
--env-file /root/.env \
|
||||
--name ${CONTAINER_NAME} \
|
||||
$ECR_REPO
|
||||
$FULL_IMAGE_NAME
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Update complete! New ${ENV} container is running."
|
||||
@@ -83,4 +86,11 @@ if [ $? -eq 0 ]; then
|
||||
echo "Cleanup complete."
|
||||
else
|
||||
echo "Failed to start container"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "======================================================"
|
||||
echo "✅ SERVER UPDATE COMPLETED SUCCESSFULLY"
|
||||
echo "Container name: ${CONTAINER_NAME}"
|
||||
echo "Image: ${FULL_IMAGE_NAME}"
|
||||
echo "======================================================"
|
||||
Reference in New Issue
Block a user