Skip to content

Instantly share code, notes, and snippets.

@witzatom
Last active May 31, 2018 09:17
Show Gist options
  • Save witzatom/19823707e86315d74b128017b062b491 to your computer and use it in GitHub Desktop.
Save witzatom/19823707e86315d74b128017b062b491 to your computer and use it in GitHub Desktop.
Sentry docker-compose automatic bootstrap

A docker-compose with a few scripts that ensure the complete bootstrap process of a sentry server. The bootstrap will:

  • do an initial setup of the database
  • create a superuser based on environment variables
  • create projects with a project key based on environment variables

The filenames in gists cannot contain '/' to indicate a directory, so i put a | to indicate a subdirectory.

To run the bootstrap first set the environment variables neccessary via:

source ./env.sh

and then simply:

docker-compose up -d

If you want to access sentry from outside of your system, you have to change system.url-prefix in sentry/config.yml to the correct url. Restarting the docker-compose afterwards (or even only the web) should apply the change. You will most likely want to edit the system.admin-email in config.yml as well.

# Environment variables used in the docker-compose:
# SENTRY_SUPERUSER - superuser username
# SENTRY_PASSWORD - superuser password
# INST_DIR - the directory of the installation ($PWD should suffice)
# SENTRY_PROJECT_NAME - name of the created project
# SENTRY_PROJECT_PUBLIC - the public key for the project key DSN
# SENTRY_PROJECT_PRIVATE - the private key for the project key DSN
# SENTRY_SECRET_KEY - overall sentry secret key
# WAIT_FOR_TIMEOUT - how many seconds to wait for postgre to spin up (default: 60)
# SENTRY_PORT - what port will sentry run on (default: 9000)
version: '3'
services:
sentry-redis:
image: redis:3.2-alpine
restart: always
sentry-postgres:
image: postgres:10.3-alpine
restart: always
volumes:
- ${INST_DIR}/data/postgres:/var/lib/postgresql/data
sentry-web:
image: sentry:8.22
restart: always
depends_on:
- sentry-redis
- sentry-postgres
env_file: ${INST_DIR}/sentry/sentry_env
environment:
SENTRY_SECRET_KEY: ${SENTRY_SECRET_KEY}
ports:
- ${SENTRY_PORT:-9000}:9000
volumes:
- ${INST_DIR}/sentry/config.yml:/etc/sentry/config.yml
- ${INST_DIR}/sentry/sentry.conf.py:/etc/sentry/sentry.conf.py
- ${INST_DIR}/sentry/scripts/:/etc/sentry/scripts/
- ${INST_DIR}/sentry/wait-for-it.sh:/bin/wait-for-it
- ${INST_DIR}/data/sentry/media:/tmp/sentry-files
entrypoint: /bin/bash
command:
- /bin/wait-for-it
- --host=sentry-postgres
- --port=5432
- --timeout=${WAIT_FOR_TIMEOUT:-60}
- --
- /bin/bash
- -c
- "sentry upgrade --noinput &&
python /etc/sentry/scripts/create_superuser.py ${SENTRY_SUPERUSER} ${SENTRY_PASSWORD} &&
python /etc/sentry/scripts/create_project.py 2 ${SENTRY_PROJECT_NAME} ${SENTRY_PROJECT_PUBLIC} ${SENTRY_PROJECT_PRIVATE} &&
sentry run web
"
sentry-cron:
image: sentry:8.22
restart: always
depends_on:
- sentry-redis
- sentry-postgres
env_file: ${INST_DIR}/sentry/sentry_env
environment:
SENTRY_SECRET_KEY: ${SENTRY_SECRET_KEY}
volumes:
- ${INST_DIR}/sentry/config.yml:/etc/sentry/config.yml
- ${INST_DIR}/sentry/sentry.conf.py:/etc/sentry/sentry.conf.py
command: "sentry run cron"
sentry-worker:
image: sentry:8.22
restart: always
depends_on:
- sentry-redis
- sentry-postgres
env_file: ${INST_DIR}/sentry/sentry_env
environment:
SENTRY_SECRET_KEY: ${SENTRY_SECRET_KEY}
volumes:
- ${INST_DIR}/sentry/config.yml:/etc/sentry/config.yml
- ${INST_DIR}/sentry/sentry.conf.py:/etc/sentry/sentry.conf.py
command: "sentry run worker"
# feel free to change these
export SENTRY_SUPERUSER=sentry
export SENTRY_PASSWORD=sentry
export SENTRY_PROJECT_NAME=ExampleProject
# these are just being generated, beware of changing these
export INST_DIR=$PWD
export SENTRY_PROJECT_PUBLIC=$(cat /dev/urandom | tr -dc 'a-f0-9' | fold -w 32 | head -n 1)
export SENTRY_PROJECT_PRIVATE=$(cat /dev/urandom | tr -dc 'a-f0-9' | fold -w 32 | head -n 1)
export SENTRY_SECRET_KEY=$(cat /dev/urandom | tr -dc 'a-f0-9' | fold -w 32 | head -n 1)
# While a lot of configuration in Sentry can be changed via the UI, for all
# new-style config (as of 8.0) you can also declare values here in this file
# to enforce defaults or to ensure they cannot be changed via the UI. For more
# information see the Sentry documentation.
###############
# Mail Server #
###############
mail.backend: 'dummy'
#'%(mail.backend)s' # Use dummy if you want to disable email entirely
# mail.host: 'localhost'
# mail.port: 25
# mail.username: ''
# mail.password: ''
# mail.use-tls: false
# The email address to send on behalf of
# mail.from: 'root@localhost'
# If you'd like to configure email replies, enable this.
# mail.enable-replies: false
# When email-replies are enabled, this value is used in the Reply-To header
# mail.reply-hostname: ''
# If you're using mailgun for inbound mail, set your API key and configure a
# route to forward to /api/hooks/mailgun/inbound/
# mail.mailgun-api-key: ''
###################
# System Settings #
###################
# If this file ever becomes compromised, it's important to regenerate your a new key
# Changing this value will result in all current sessions being invalidated.
# A new key can be generated with `$ sentry config generate-secret-key`
system.secret-key: '%(secret_key)s'
# The ``redis.clusters`` setting is used, unsurprisingly, to configure Redis
# clusters. These clusters can be then referred to by name when configuring
# backends such as the cache, digests, or TSDB backend.
#
# Two types of clusters are currently supported:
#
# rb.Cluster
# A redis blaster cluster is the traditional cluster used by most services
# within sentry. This is the default type cluster type.
#
# rediscluster.StrictRedisCluster
# An official Redis Cluster can be configured by marking the named group with
# the ``is_redis_cluster: True`` flag. In future versions of Sentry more
# services will require this type of cluster.
#
#redis.clusters:
# default:
# hosts:
# 0:
# host: 127.0.0.1
# port: 6379
system.admin-email: '[email protected]'
# TODO this is really annoying, i dont know how to set this automatically, at least the port
system.url-prefix: 'http://localhost:9000'
################
# File storage #
################
# Uploaded media uses these `filestore` settings. The available
# backends are either `filesystem` or `s3`.
# TODO mount a volume for uploaded media
filestore.backend: 'filesystem'
filestore.options:
location: '/tmp/sentry-files'
# filestore.backend: 's3'
# filestore.options:
# access_key: 'AKIXXXXXX'
# secret_key: 'XXXXXXX'
# bucket_name: 's3-bucket-name'
import sys
from sentry.runner import configure
configure()
from sentry.models import Project, ProjectKey
project_id = sys.argv[1]
project_name = sys.argv[2]
public_key = sys.argv[3]
secret_key = sys.argv[4]
try:
p = Project.objects.get(id=project_id)
print("Project exists, skipping creation")
ProjectKey.objects.get(project=p).delete()
ProjectKey.objects.create(project=p, public_key=public_key, secret_key=secret_key, roles=1)
except Exception as e:
try:
slug = project_name.lower()
p = Project.objects.create(id=project_id, name=project_name, public=False, slug=slug, team_id=1,
organization_id=1)
ProjectKey.objects.get(project=p).delete()
ProjectKey.objects.create(project=p, public_key=public_key, secret_key=secret_key, roles=1)
except Exception as e:
print(e)
import sys
from sentry.runner import configure, call_command
configure()
from sentry.models import User
username = sys.argv[1]
password = sys.argv[2]
try:
existing_user = User.objects.get(username=username)
print("User exists, skipping creation")
except:
call_command('sentry.runner.commands.createuser.createuser', email=username, password=password, superuser=True,
no_input=True)
# This file is just Python, with a touch of Django which means
# you can inherit and tweak settings to your hearts content.
# For Docker, the following environment variables are supported:
# SENTRY_POSTGRES_HOST
# SENTRY_POSTGRES_PORT
# SENTRY_DB_NAME
# SENTRY_DB_USER
# SENTRY_DB_PASSWORD
# SENTRY_RABBITMQ_HOST
# SENTRY_RABBITMQ_USERNAME
# SENTRY_RABBITMQ_PASSWORD
# SENTRY_RABBITMQ_VHOST
# SENTRY_REDIS_HOST
# SENTRY_REDIS_PASSWORD
# SENTRY_REDIS_PORT
# SENTRY_REDIS_DB
# SENTRY_MEMCACHED_HOST
# SENTRY_MEMCACHED_PORT
# SENTRY_FILESTORE_DIR
# SENTRY_SERVER_EMAIL
# SENTRY_EMAIL_HOST
# SENTRY_EMAIL_PORT
# SENTRY_EMAIL_USER
# SENTRY_EMAIL_PASSWORD
# SENTRY_EMAIL_USE_TLS
# SENTRY_ENABLE_EMAIL_REPLIES
# SENTRY_SMTP_HOSTNAME
# SENTRY_MAILGUN_API_KEY
# SENTRY_SINGLE_ORGANIZATION
# SENTRY_SECRET_KEY
# GITHUB_APP_ID
# GITHUB_API_SECRET
# BITBUCKET_CONSUMER_KEY
# BITBUCKET_CONSUMER_SECRET
from sentry.conf.server import * # NOQA
import os
import os.path
CONF_ROOT = os.path.dirname(__file__)
postgres = env('SENTRY_POSTGRES_HOST') or (env('POSTGRES_PORT_5432_TCP_ADDR') and 'postgres')
if postgres:
DATABASES = {
'default': {
'ENGINE': 'sentry.db.postgres',
'NAME': (
env('SENTRY_DB_NAME')
or env('POSTGRES_ENV_POSTGRES_USER')
or 'postgres'
),
'USER': (
env('SENTRY_DB_USER')
or env('POSTGRES_ENV_POSTGRES_USER')
or 'postgres'
),
'PASSWORD': (
env('SENTRY_DB_PASSWORD')
or env('POSTGRES_ENV_POSTGRES_PASSWORD')
or ''
),
'HOST': postgres,
'PORT': (
env('SENTRY_POSTGRES_PORT')
or ''
),
'OPTIONS': {
'autocommit': True,
},
},
}
# You should not change this setting after your database has been created
# unless you have altered all schemas first
SENTRY_USE_BIG_INTS = True
# If you're expecting any kind of real traffic on Sentry, we highly recommend
# configuring the CACHES and Redis settings
###########
# General #
###########
# Instruct Sentry that this install intends to be run by a single organization
# and thus various UI optimizations should be enabled.
SENTRY_SINGLE_ORGANIZATION = env('SENTRY_SINGLE_ORGANIZATION', True)
#########
# Redis #
#########
# Generic Redis configuration used as defaults for various things including:
# Buffers, Quotas, TSDB
redis = env('SENTRY_REDIS_HOST') or (env('REDIS_PORT_6379_TCP_ADDR') and 'redis')
if not redis:
raise Exception('Error: REDIS_PORT_6379_TCP_ADDR (or SENTRY_REDIS_HOST) is undefined, did you forget to `--link` a redis container?')
redis_password = env('SENTRY_REDIS_PASSWORD') or ''
redis_port = env('SENTRY_REDIS_PORT') or '6379'
redis_db = env('SENTRY_REDIS_DB') or '0'
SENTRY_OPTIONS.update({
'redis.clusters': {
'default': {
'hosts': {
0: {
'host': redis,
'password': redis_password,
'port': redis_port,
'db': redis_db,
},
},
},
},
})
#########
# Cache #
#########
# Sentry currently utilizes two separate mechanisms. While CACHES is not a
# requirement, it will optimize several high throughput patterns.
memcached = env('SENTRY_MEMCACHED_HOST') or (env('MEMCACHED_PORT_11211_TCP_ADDR') and 'memcached')
if memcached:
memcached_port = (
env('SENTRY_MEMCACHED_PORT')
or '11211'
)
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': [memcached + ':' + memcached_port],
'TIMEOUT': 3600,
}
}
# A primary cache is required for things such as processing events
SENTRY_CACHE = 'sentry.cache.redis.RedisCache'
#########
# Queue #
#########
# See https://docs.getsentry.com/on-premise/server/queue/ for more
# information on configuring your queue broker and workers. Sentry relies
# on a Python framework called Celery to manage queues.
rabbitmq = env('SENTRY_RABBITMQ_HOST') or (env('RABBITMQ_PORT_5672_TCP_ADDR') and 'rabbitmq')
if rabbitmq:
BROKER_URL = (
'amqp://' + (
env('SENTRY_RABBITMQ_USERNAME')
or env('RABBITMQ_ENV_RABBITMQ_DEFAULT_USER')
or 'guest'
) + ':' + (
env('SENTRY_RABBITMQ_PASSWORD')
or env('RABBITMQ_ENV_RABBITMQ_DEFAULT_PASS')
or 'guest'
) + '@' + rabbitmq + '/' + (
env('SENTRY_RABBITMQ_VHOST')
or env('RABBITMQ_ENV_RABBITMQ_DEFAULT_VHOST')
or '/'
)
)
else:
BROKER_URL = 'redis://:' + redis_password + '@' + redis + ':' + redis_port + '/' + redis_db
###############
# Rate Limits #
###############
# Rate limits apply to notification handlers and are enforced per-project
# automatically.
SENTRY_RATELIMITER = 'sentry.ratelimits.redis.RedisRateLimiter'
##################
# Update Buffers #
##################
# Buffers (combined with queueing) act as an intermediate layer between the
# database and the storage API. They will greatly improve efficiency on large
# numbers of the same events being sent to the API in a short amount of time.
# (read: if you send any kind of real data to Sentry, you should enable buffers)
SENTRY_BUFFER = 'sentry.buffer.redis.RedisBuffer'
##########
# Quotas #
##########
# Quotas allow you to rate limit individual projects or the Sentry install as
# a whole.
SENTRY_QUOTAS = 'sentry.quotas.redis.RedisQuota'
########
# TSDB #
########
# The TSDB is used for building charts as well as making things like per-rate
# alerts possible.
SENTRY_TSDB = 'sentry.tsdb.redis.RedisTSDB'
###########
# Digests #
###########
# The digest backend powers notification summaries.
SENTRY_DIGESTS = 'sentry.digests.backends.redis.RedisBackend'
################
# File storage #
################
# Uploaded media uses these `filestore` settings. The available
# backends are either `filesystem` or `s3`.
SENTRY_OPTIONS['filestore.backend'] = 'filesystem'
SENTRY_OPTIONS['filestore.options'] = {
'location': env('SENTRY_FILESTORE_DIR'),
}
##############
# Web Server #
##############
# If you're using a reverse SSL proxy, you should enable the X-Forwarded-Proto
# header and set `SENTRY_USE_SSL=1`
if env('SENTRY_USE_SSL', False):
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SOCIAL_AUTH_REDIRECT_IS_HTTPS = True
SENTRY_WEB_HOST = '0.0.0.0'
SENTRY_WEB_PORT = 9000
SENTRY_WEB_OPTIONS = {
# 'workers': 3, # the number of web workers
}
###############
# Mail Server #
###############
email = env('SENTRY_EMAIL_HOST') or (env('SMTP_PORT_25_TCP_ADDR') and 'smtp')
if email:
SENTRY_OPTIONS['mail.backend'] = 'smtp'
SENTRY_OPTIONS['mail.host'] = email
SENTRY_OPTIONS['mail.password'] = env('SENTRY_EMAIL_PASSWORD') or ''
SENTRY_OPTIONS['mail.username'] = env('SENTRY_EMAIL_USER') or ''
SENTRY_OPTIONS['mail.port'] = int(env('SENTRY_EMAIL_PORT') or 25)
SENTRY_OPTIONS['mail.use-tls'] = env('SENTRY_EMAIL_USE_TLS', False)
else:
SENTRY_OPTIONS['mail.backend'] = 'dummy'
# The email address to send on behalf of
SENTRY_OPTIONS['mail.from'] = env('SENTRY_SERVER_EMAIL') or 'root@localhost'
# If you're using mailgun for inbound mail, set your API key and configure a
# route to forward to /api/hooks/mailgun/inbound/
SENTRY_OPTIONS['mail.mailgun-api-key'] = env('SENTRY_MAILGUN_API_KEY') or ''
# If you specify a MAILGUN_API_KEY, you definitely want EMAIL_REPLIES
if SENTRY_OPTIONS['mail.mailgun-api-key']:
SENTRY_OPTIONS['mail.enable-replies'] = True
else:
SENTRY_OPTIONS['mail.enable-replies'] = env('SENTRY_ENABLE_EMAIL_REPLIES', False)
if SENTRY_OPTIONS['mail.enable-replies']:
SENTRY_OPTIONS['mail.reply-hostname'] = env('SENTRY_SMTP_HOSTNAME') or ''
# If this value ever becomes compromised, it's important to regenerate your
# SENTRY_SECRET_KEY. Changing this value will result in all current sessions
# being invalidated.
secret_key = env('SENTRY_SECRET_KEY')
if not secret_key:
raise Exception('Error: SENTRY_SECRET_KEY is undefined, run `generate-secret-key` and set to -e SENTRY_SECRET_KEY')
if 'SENTRY_RUNNING_UWSGI' not in os.environ and len(secret_key) < 32:
print('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
print('!! CAUTION !!')
print('!! Your SENTRY_SECRET_KEY is potentially insecure. !!')
print('!! We recommend at least 32 characters long. !!')
print('!! Regenerate with `generate-secret-key`. !!')
print('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
SENTRY_OPTIONS['system.secret-key'] = secret_key
if 'GITHUB_APP_ID' in os.environ:
GITHUB_EXTENDED_PERMISSIONS = ['repo']
GITHUB_APP_ID = env('GITHUB_APP_ID')
GITHUB_API_SECRET = env('GITHUB_API_SECRET')
if 'BITBUCKET_CONSUMER_KEY' in os.environ:
BITBUCKET_CONSUMER_KEY = env('BITBUCKET_CONSUMER_KEY')
BITBUCKET_CONSUMER_SECRET = env('BITBUCKET_CONSUMER_SECRET')
SENTRY_FEATURES['auth:register'] = False
SENTRY_BEACON = True
# DO NOT CHANGE THESE
SENTRY_POSTGRES_HOST=sentry-postgres
SENTRY_REDIS_HOST=sentry-redis
SENTRY_DB_USER=postgres
SENTRY_DB_PASSWORD=""
SENTRY_DISABLE_REGISTRATION=True
# Unused
# [email protected]
# SENTRY_EMAIL_HOST=smtp.sparkpostmail.com
# SENTRY_EMAIL_PASSWORD="smtp password"
# SENTRY_EMAIL_USER=SMTP_Injection
# SENTRY_EMAIL_PORT=587
# SENTRY_EMAIL_USE_TLS=True
#!/usr/bin/env bash
# Use this script to test if a given TCP host/port are available
cmdname=$(basename $0)
echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
usage()
{
cat << USAGE >&2
Usage:
$cmdname host:port [-s] [-t timeout] [-- command args]
-h HOST | --host=HOST Host or IP under test
-p PORT | --port=PORT TCP port under test
Alternatively, you specify the host and port as host:port
-s | --strict Only execute subcommand if the test succeeds
-q | --quiet Don't output any status messages
-t TIMEOUT | --timeout=TIMEOUT
Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit 1
}
wait_for()
{
if [[ $TIMEOUT -gt 0 ]]; then
echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT"
else
echoerr "$cmdname: waiting for $HOST:$PORT without a timeout"
fi
start_ts=$(date +%s)
while :
do
if [[ $ISBUSY -eq 1 ]]; then
nc -z $HOST $PORT
result=$?
else
(echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1
result=$?
fi
if [[ $result -eq 0 ]]; then
end_ts=$(date +%s)
echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds"
break
fi
sleep 1
done
return $result
}
wait_for_wrapper()
{
# In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
if [[ $QUIET -eq 1 ]]; then
timeout $BUSYTIMEFLAG $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
else
timeout $BUSYTIMEFLAG $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
fi
PID=$!
trap "kill -INT -$PID" INT
wait $PID
RESULT=$?
if [[ $RESULT -ne 0 ]]; then
echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT"
fi
return $RESULT
}
# process arguments
while [[ $# -gt 0 ]]
do
case "$1" in
*:* )
hostport=(${1//:/ })
HOST=${hostport[0]}
PORT=${hostport[1]}
shift 1
;;
--child)
CHILD=1
shift 1
;;
-q | --quiet)
QUIET=1
shift 1
;;
-s | --strict)
STRICT=1
shift 1
;;
-h)
HOST="$2"
if [[ $HOST == "" ]]; then break; fi
shift 2
;;
--host=*)
HOST="${1#*=}"
shift 1
;;
-p)
PORT="$2"
if [[ $PORT == "" ]]; then break; fi
shift 2
;;
--port=*)
PORT="${1#*=}"
shift 1
;;
-t)
TIMEOUT="$2"
if [[ $TIMEOUT == "" ]]; then break; fi
shift 2
;;
--timeout=*)
TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
CLI=("$@")
break
;;
--help)
usage
;;
*)
echoerr "Unknown argument: $1"
usage
;;
esac
done
if [[ "$HOST" == "" || "$PORT" == "" ]]; then
echoerr "Error: you need to provide a host and port to test."
usage
fi
TIMEOUT=${TIMEOUT:-15}
STRICT=${STRICT:-0}
CHILD=${CHILD:-0}
QUIET=${QUIET:-0}
# check to see if timeout is from busybox?
# check to see if timeout is from busybox?
TIMEOUT_PATH=$(realpath $(which timeout))
if [[ $TIMEOUT_PATH =~ "busybox" ]]; then
ISBUSY=1
BUSYTIMEFLAG="-t"
else
ISBUSY=0
BUSYTIMEFLAG=""
fi
if [[ $CHILD -gt 0 ]]; then
wait_for
RESULT=$?
exit $RESULT
else
if [[ $TIMEOUT -gt 0 ]]; then
wait_for_wrapper
RESULT=$?
else
wait_for
RESULT=$?
fi
fi
if [[ $CLI != "" ]]; then
if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then
echoerr "$cmdname: strict mode, refusing to execute subprocess"
exit $RESULT
fi
exec "${CLI[@]}"
else
exit $RESULT
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment